diff --git a/config/sync/views.view.recent_daily_emails.yml b/config/sync/views.view.recent_daily_emails.yml new file mode 100644 index 000000000..6541baabf --- /dev/null +++ b/config/sync/views.view.recent_daily_emails.yml @@ -0,0 +1,286 @@ +uuid: 144c6b85-87cc-4c99-8f46-8a43fff416bf +langcode: en +status: true +dependencies: + config: + - node.type.daily_email + module: + - node + - user +id: recent_daily_emails +label: 'Recent daily emails' +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +display: + default: + id: default + display_title: Default + display_plugin: default + position: 0 + display_options: + title: 'Recent daily emails' + fields: + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + plugin_id: field + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + created: + id: created + table: node_field_data + field: created + relationship: none + group_type: group + admin_label: '' + entity_type: node + entity_field: created + plugin_id: field + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: timestamp + settings: + date_format: short + custom_date_format: '' + timezone: '' + tooltip: + date_format: long + custom_date_format: '' + time_diff: + enabled: false + future_format: '@interval hence' + past_format: '@interval ago' + granularity: 2 + refresh: 60 + description: '' + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + pager: + type: some + options: + offset: 0 + items_per_page: 7 + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + empty: { } + sorts: + created: + id: created + table: node_field_data + field: created + relationship: none + group_type: group + admin_label: '' + entity_type: node + entity_field: created + plugin_id: date + order: DESC + expose: + label: '' + field_identifier: '' + exposed: false + granularity: second + arguments: { } + filters: + status: + id: status + table: node_field_data + field: status + entity_type: node + entity_field: status + plugin_id: boolean + value: '1' + group: 1 + expose: + operator: '' + type: + id: type + table: node_field_data + field: type + entity_type: node + entity_field: type + plugin_id: bundle + value: + daily_email: daily_email + style: + type: html_list + row: + type: fields + options: + default_field_elements: true + inline: + title: title + created: created + separator: ' - ' + hide_empty: false + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } + header: + area: + id: area + table: views + field: area + relationship: none + group_type: group + admin_label: '' + plugin_id: text + empty: false + content: + value: 'These are the emails I sent this week:' + format: markdown + tokenize: false + footer: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } + block_1: + id: block_1 + display_title: Block + display_plugin: block + position: 1 + display_options: + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/content/block_content.262bcabc-61d8-4b67-b4ab-ba39d86db38d.json b/content/block_content.262bcabc-61d8-4b67-b4ab-ba39d86db38d.json new file mode 100644 index 000000000..7b82c54fa --- /dev/null +++ b/content/block_content.262bcabc-61d8-4b67-b4ab-ba39d86db38d.json @@ -0,0 +1,63 @@ +{ + "uuid": [ + { + "value": "262bcabc-61d8-4b67-b4ab-ba39d86db38d" + } + ], + "langcode": [ + { + "value": "en" + } + ], + "type": [ + { + "target_id": "basic", + "target_type": "block_content_type", + "target_uuid": "2646e684-c917-4dd0-b502-1a73b9c10131" + } + ], + "revision_created": [ + { + "value": "2025-07-03T15:10:16+00:00" + } + ], + "revision_user": [], + "status": [ + { + "value": true + } + ], + "info": [ + { + "value": "Introduction text" + } + ], + "changed": [ + { + "value": "2025-07-03T15:10:16+00:00" + } + ], + "reusable": [ + { + "value": false + } + ], + "default_langcode": [ + { + "value": true + } + ], + "revision_translation_affected": [ + { + "value": true + } + ], + "body": [ + { + "value": "Subscribe to my daily newsletter for software professionals on software development and delivery, Drupal, DevOps, community, and open-source.", + "format": "markdown", + "processed": "

Subscribe to my daily newsletter for software professionals on software development and delivery, Drupal, DevOps, community, and open-source.<\/p>\n", + "summary": "" + } + ] +} \ No newline at end of file diff --git a/content/block_content.f451cc6b-1555-4bb2-ad3c-6d1bb4ee6bf5.json b/content/block_content.f451cc6b-1555-4bb2-ad3c-6d1bb4ee6bf5.json new file mode 100644 index 000000000..368a96e93 --- /dev/null +++ b/content/block_content.f451cc6b-1555-4bb2-ad3c-6d1bb4ee6bf5.json @@ -0,0 +1,63 @@ +{ + "uuid": [ + { + "value": "f451cc6b-1555-4bb2-ad3c-6d1bb4ee6bf5" + } + ], + "langcode": [ + { + "value": "en" + } + ], + "type": [ + { + "target_id": "basic", + "target_type": "block_content_type", + "target_uuid": "2646e684-c917-4dd0-b502-1a73b9c10131" + } + ], + "revision_created": [ + { + "value": "2025-07-03T15:11:02+00:00" + } + ], + "revision_user": [], + "status": [ + { + "value": true + } + ], + "info": [ + { + "value": "Browse the archive" + } + ], + "changed": [ + { + "value": "2025-07-03T15:11:02+00:00" + } + ], + "reusable": [ + { + "value": false + } + ], + "default_langcode": [ + { + "value": true + } + ], + "revision_translation_affected": [ + { + "value": true + } + ], + "body": [ + { + "value": "Not sure? [Browse the archive \u2192](\/archive)", + "format": "markdown", + "processed": "

Not sure? Browse the archive \u2192<\/a><\/p>\n", + "summary": "" + } + ] +} \ No newline at end of file diff --git a/content/meta/index.json b/content/meta/index.json index 34daf7da5..4ae22caf2 100644 --- a/content/meta/index.json +++ b/content/meta/index.json @@ -6625,5 +6625,13 @@ ], "path_alias.dfdfc073-dc09-49ae-a325-aacebe1911b6": [ "node.465a0fa7-eb20-4d3e-b30d-9d03187b13ca" - ] + ], + "node.57c15821-f744-45ce-960e-5f77d41c3ad3": [ + "user.b8966985-d4b2-42a7-a319-2e94ccfbb849" + ], + "path_alias.0d01ff0e-6edd-4b9e-a93d-cf831bb6212f": [ + "node.57c15821-f744-45ce-960e-5f77d41c3ad3" + ], + "block_content.262bcabc-61d8-4b67-b4ab-ba39d86db38d": [], + "block_content.f451cc6b-1555-4bb2-ad3c-6d1bb4ee6bf5": [] } \ No newline at end of file diff --git a/content/node.57c15821-f744-45ce-960e-5f77d41c3ad3.json b/content/node.57c15821-f744-45ce-960e-5f77d41c3ad3.json new file mode 100644 index 000000000..130715ee1 --- /dev/null +++ b/content/node.57c15821-f744-45ce-960e-5f77d41c3ad3.json @@ -0,0 +1,95 @@ +{ + "uuid": [ + { + "value": "57c15821-f744-45ce-960e-5f77d41c3ad3" + } + ], + "langcode": [ + { + "value": "en" + } + ], + "type": [ + { + "target_id": "page", + "target_type": "node_type", + "target_uuid": "a6dfb70f-21fc-46e0-aa8c-45c5d0cef75f" + } + ], + "revision_timestamp": [ + { + "value": "2025-07-03T16:12:47+00:00" + } + ], + "revision_uid": [ + { + "target_type": "user", + "target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849" + } + ], + "revision_log": [], + "status": [ + { + "value": true + } + ], + "uid": [ + { + "target_type": "user", + "target_uuid": "b8966985-d4b2-42a7-a319-2e94ccfbb849" + } + ], + "title": [ + { + "value": "Sign up for the Daily Drupaler Email List" + } + ], + "created": [ + { + "value": "2025-07-03T15:08:56+00:00" + } + ], + "changed": [ + { + "value": "2025-07-03T16:12:47+00:00" + } + ], + "promote": [ + { + "value": false + } + ], + "sticky": [ + { + "value": false + } + ], + "default_langcode": [ + { + "value": true + } + ], + "revision_translation_affected": [ + { + "value": true + } + ], + "path": [ + { + "alias": "\/daily", + "langcode": "en" + } + ], + "body": [], + "field_seo_analysis": [ + { + "status": "0", + "focus_keyword": "", + "title": null, + "description": null + } + ], + "field_seo_description": [], + "field_seo_image": [], + "field_seo_title": [] +} \ No newline at end of file diff --git a/content/path_alias.0d01ff0e-6edd-4b9e-a93d-cf831bb6212f.json b/content/path_alias.0d01ff0e-6edd-4b9e-a93d-cf831bb6212f.json new file mode 100644 index 000000000..011f041d2 --- /dev/null +++ b/content/path_alias.0d01ff0e-6edd-4b9e-a93d-cf831bb6212f.json @@ -0,0 +1,27 @@ +{ + "uuid": [ + { + "value": "0d01ff0e-6edd-4b9e-a93d-cf831bb6212f" + } + ], + "langcode": [ + { + "value": "en" + } + ], + "path": [ + { + "value": "\/node\/57c15821-f744-45ce-960e-5f77d41c3ad3" + } + ], + "alias": [ + { + "value": "\/daily" + } + ], + "status": [ + { + "value": true + } + ] +} \ No newline at end of file diff --git a/modules/opd_daily_emails/opd_daily_emails.module b/modules/opd_daily_emails/opd_daily_emails.module index f6ead9a94..e8c32b8a2 100644 --- a/modules/opd_daily_emails/opd_daily_emails.module +++ b/modules/opd_daily_emails/opd_daily_emails.module @@ -30,6 +30,20 @@ function opd_daily_emails_entity_presave(Drupal\Core\Entity\EntityInterface $ent \Drupal::service(AddRandomCtaToDailyEmail::class)($entity); } +/** + * Implements hook_form_FORM_ID_alter(). + * + * @param array{'#action': string, '#attributes': array} $form + */ +function opd_daily_emails_form_opd_daily_emails_kit_subscription_form_alter(array &$form): void { + $form['#action'] = 'https://app.convertkit.com/forms/3546728/subscriptions'; + + $form['#attributes']['data-format'] = 'inline'; + $form['#attributes']['data-sv-form'] = '3546728'; + $form['#attributes']['data-uid'] = 'f0c1d2b57f'; + $form['#attributes']['data-version'] = 5; +} + /** * Implements hook_token_info(). * diff --git a/modules/opd_daily_emails/src/Form/KitSubscriptionForm.php b/modules/opd_daily_emails/src/Form/KitSubscriptionForm.php new file mode 100644 index 000000000..ffa669f63 --- /dev/null +++ b/modules/opd_daily_emails/src/Form/KitSubscriptionForm.php @@ -0,0 +1,48 @@ + $form + * @param FormStateInterface $formState + * + * @return array + */ + public function buildForm(array $form, FormStateInterface $formState): array { + $form['email_address'] = [ + '#placeholder' => 'me@example.com', + '#title' => $this->t('What is your best email address?'), + '#type' => 'email', + ]; + + $form['actions']['submit'] = [ + '#attributes' => [ + 'class' => ['inline-flex justify-center items-center py-3 px-6 w-full font-medium text-white no-underline rounded-md border duration-200 ease-in-out hover:bg-white focus:bg-white border-blue-primary bg-blue-primary transition-color hover:text-blue-primary focus:text-blue-primary'], + 'data-element' => 'submit', + ], + '#type' => 'button', + '#value' => $this->t('Get daily emails') . ' →', + ]; + + return $form; + } + + /** + * @param array $form + * @param FormStateInterface $formState + */ + public function submitForm(array &$form, FormStateInterface $formState): void { + } + +} diff --git a/modules/opd_daily_emails/src/Plugin/Block/KitSubscriptionFormBlock.php b/modules/opd_daily_emails/src/Plugin/Block/KitSubscriptionFormBlock.php new file mode 100644 index 000000000..561c592df --- /dev/null +++ b/modules/opd_daily_emails/src/Plugin/Block/KitSubscriptionFormBlock.php @@ -0,0 +1,59 @@ + $configuration + * @param string $pluginId + * @param array $pluginDefinition + * @param FormBuilderInterface $formBuilder + */ + public function __construct( + array $configuration, + string $pluginId, + array $pluginDefinition, + private FormBuilderInterface $formBuilder, + ) { + parent::__construct($configuration, $pluginId, $pluginDefinition); + } + + /** + * @return array + */ + public function build(): array { + return $this->formBuilder->getForm(KitSubscriptionForm::class); + } + + /** + * @param ContainerInterface $container + * @param array $configuration + * @param string $pluginId + * @param array $pluginDefinition + */ + public static function create(ContainerInterface $container, array $configuration, $pluginId, $pluginDefinition): self { + return new static( + $configuration, + $pluginId, + $pluginDefinition, + $container->get(FormBuilderInterface::class), + ); + } + +} diff --git a/themes/opdavies/css/tailwind.css b/themes/opdavies/css/tailwind.css index 486b831ae..24b42620e 100644 --- a/themes/opdavies/css/tailwind.css +++ b/themes/opdavies/css/tailwind.css @@ -1,5 +1,6 @@ @import "tailwindcss"; +@plugin '@tailwindcss/forms'; @plugin '@tailwindcss/typography'; @theme { diff --git a/themes/opdavies/templates/form-element-label.html.twig b/themes/opdavies/templates/form-element-label.html.twig new file mode 100644 index 000000000..80d435089 --- /dev/null +++ b/themes/opdavies/templates/form-element-label.html.twig @@ -0,0 +1,28 @@ +{# +/** + * @file + * Default theme implementation for a form element label. + * + * Available variables: + * - title: The label's text. + * - title_display: Elements title_display setting. + * - required: An indicator for whether the associated form element is required. + * - attributes: A list of HTML attributes for the label. + * + * @see template_preprocess_form_element_label() + * + * @ingroup themeable + */ +#} +{% + set classes = [ + 'text-lg', + title_display == 'after' ? 'option', + title_display == 'invisible' ? 'visually-hidden', + required ? 'js-form-required', + required ? 'form-required', + ] +%} +{% if title is not empty or required -%} + {{ title }} +{%- endif %} diff --git a/themes/opdavies/templates/input--email.html.twig b/themes/opdavies/templates/input--email.html.twig new file mode 100644 index 000000000..c71094101 --- /dev/null +++ b/themes/opdavies/templates/input--email.html.twig @@ -0,0 +1,15 @@ +{# +/** + * @file + * Default theme implementation for an 'input' #type form element. + * + * Available variables: + * - attributes: A list of HTML attributes for the input element. + * - children: Optional additional rendered elements. + * + * @see template_preprocess_input() + * + * @ingroup themeable + */ +#} +{{ children }} diff --git a/themes/opdavies/templates/input--submit.html.twig b/themes/opdavies/templates/input--submit.html.twig new file mode 100644 index 000000000..d0a80af55 --- /dev/null +++ b/themes/opdavies/templates/input--submit.html.twig @@ -0,0 +1,17 @@ +{# +/** + * @file + * Default theme implementation for an 'input' #type form element. + * + * Available variables: + * - attributes: A list of HTML attributes for the input element. + * - children: Optional additional rendered elements. + * + * @see template_preprocess_input() + * + * @ingroup themeable + */ +#} +

+ {{ children }} +
diff --git a/themes/opdavies/templates/node--page.html.twig b/themes/opdavies/templates/node--page.html.twig new file mode 100644 index 000000000..416768bf7 --- /dev/null +++ b/themes/opdavies/templates/node--page.html.twig @@ -0,0 +1,87 @@ +{# +/** + * @file + * Default theme implementation to display a node. + * + * Available variables: + * - node: The node entity with limited access to object properties and methods. + * Only method names starting with "get", "has", or "is" and a few common + * methods such as "id", "label", and "bundle" are available. For example: + * - node.getCreatedTime() will return the node creation timestamp. + * - node.hasField('field_example') returns TRUE if the node bundle includes + * field_example. (This does not indicate the presence of a value in this + * field.) + * - node.isPublished() will return whether the node is published or not. + * Calling other methods, such as node.delete(), will result in an exception. + * See \Drupal\node\Entity\Node for a full list of public properties and + * methods for the node object. + * - label: (optional) The title of the node. + * - content: All node items. Use {{ content }} to print them all, + * or print a subset such as {{ content.field_example }}. Use + * {{ content|without('field_example') }} to temporarily suppress the printing + * of a given child element. + * - author_picture: The node author user entity, rendered using the "compact" + * view mode. + * - date: (optional) Themed creation date field. + * - author_name: (optional) Themed author name field. + * - url: Direct URL of the current node. + * - display_submitted: Whether submission information should be displayed. + * - attributes: HTML attributes for the containing element. + * The attributes.class element may contain one or more of the following + * classes: + * - node: The current template type (also known as a "theming hook"). + * - node--type-[type]: The current node type. For example, if the node is an + * "Article" it would result in "node--type-article". Note that the machine + * name will often be in a short form of the human readable label. + * - node--view-mode-[view_mode]: The View Mode of the node; for example, a + * teaser would result in: "node--view-mode-teaser", and + * full: "node--view-mode-full". + * The following are controlled through the node publishing options. + * - node--promoted: Appears on nodes promoted to the front page. + * - node--sticky: Appears on nodes ordered above other non-sticky nodes in + * teaser listings. + * - node--unpublished: Appears on unpublished nodes visible only to site + * admins. + * - title_attributes: Same as attributes, except applied to the main title + * tag that appears in the template. + * - content_attributes: Same as attributes, except applied to the main + * content tag that appears in the template. + * - author_attributes: Same as attributes, except applied to the author of + * the node tag that appears in the template. + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * - view_mode: View mode; for example, "teaser" or "full". + * - page: Flag for the full page state. Will be true if view_mode is 'full'. + * + * @see template_preprocess_node() + * + * @ingroup themeable + */ +#} + + {{ title_prefix }} + + {% if label and not page %} + + {{ label }} + + {% endif %} + + {{ title_suffix }} + + {% if display_submitted %} + + {% endif %} + + + {{ content }} + + diff --git a/themes/opdavies/templates/views-view--recent-daily-emails.html.twig b/themes/opdavies/templates/views-view--recent-daily-emails.html.twig new file mode 100644 index 000000000..4e444ec33 --- /dev/null +++ b/themes/opdavies/templates/views-view--recent-daily-emails.html.twig @@ -0,0 +1,73 @@ +{# +/** + * @file + * Default theme implementation for main view template. + * + * Available variables: + * - attributes: Remaining HTML attributes for the element. + * - css_name: A CSS-safe version of the view name. + * - css_class: The user-specified classes names, if any. + * - header: The optional header. + * - footer: The optional footer. + * - rows: The results of the view query, if any. + * - empty: The content to display if there are no rows. + * - pager: The optional pager next/prev links to display. + * - exposed: Exposed widget form/info to display. + * - feed_icons: Optional feed icons to display. + * - more: An optional link to the next page of results. + * - title: Title of the view, only used when displaying in the admin preview. + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the view title. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the view title. + * - attachment_before: An optional attachment view to be displayed before the + * view content. + * - attachment_after: An optional attachment view to be displayed after the + * view content. + * - dom_id: Unique id for every view being printed to give unique class for + * JavaScript. + * + * @see template_preprocess_views_view() + * + * @ingroup themeable + */ +#} +{% + set classes = [ + dom_id ? 'js-view-dom-id-' ~ dom_id, + ] +%} + + {{ title_prefix }} + {{ title }} + {{ title_suffix }} + +
+ {% if header %} +
+ {{ header }} +
+ {% endif %} + + {{ exposed }} + {{ attachment_before }} + + {% if rows -%} + {{ rows }} + {% elseif empty -%} + {{ empty }} + {% endif %} + {{ pager }} + + {{ attachment_after }} + {{ more }} + + {% if footer %} + + {% endif %} + + {{ feed_icons }} +
+