diff --git a/Upendo-Video-Resource-List/README.md b/Upendo-Video-Resource-List/README.md new file mode 100644 index 0000000..cfdac58 --- /dev/null +++ b/Upendo-Video-Resource-List/README.md @@ -0,0 +1,51 @@ +# Upendo Video Resource List + +Independent DNN/OpenContent template for a video resources page. It renders a simple responsive video resources section using privacy-friendly YouTube embeds when a valid YouTube ID can be parsed. + +## Quick Path + +1. Add an OpenContent module to `/resources/videos`. +2. Select `Upendo-Video-Resource-List` and load the default data if needed. +3. Replace the draft sample videos with approved titles, descriptions, YouTube URLs, thumbnails, categories, and tags. + +## Layout Settings + +- `BackgroundColorClass`: controls the outer section background. Includes theme-friendly labels such as `Soft Gray` for `bg-soft-gray`. +- `ContainerClass`: controls the inner content width. Default is `container-xxl`; runtime accepts `container`, `container-sm`, `container-md`, `container-lg`, `container-xl`, `container-xxl`, and `container-fluid`. +- `DisplayMode`: `grouped` keeps category sections and anchor navigation; `grid` renders all videos in one unified card grid. +- `CardsPerRow`: choose `2` or `3` desktop columns. Mobile always renders one column. +- `CompactSpacing`: reduces intro, category, and card gaps for denser pages. +- `ShowBackToTop`: controls the small category heading link in grouped mode. + +## Card Layout Behavior + +- Cards in the same desktop row are designed to keep their internal content visually aligned. +- Category and title areas reserve consistent vertical space on desktop so descriptions start at a predictable rhythm. +- Tags are anchored near the lower action area, directly above the CTA button. +- CTA buttons stay at the bottom of each card when cards have different title or description lengths. +- Mobile cards relax the desktop alignment rules to avoid unnecessary empty space on narrow screens. + +## Content Editing Notes + +- `ModuleAnchor` accepts letters, numbers, and hyphens only. +- `YouTubeUrl` supports common watch, `youtu.be`, `/embed/`, and `/shorts/` URL formats. +- Valid YouTube URLs render through `https://www.youtube-nocookie.com/embed/{id}`. +- `ThumbnailImage` is a fallback for missing or unparseable YouTube URLs. +- `Tags` is a comma-separated text field, intentionally not a lookup, so the template remains independent. +- `_blank` CTA links automatically render with `rel="noopener noreferrer"`. +- Add multiple videos with the same `Category` to render multiple cards inside one grouped section. + +## Accessibility + +- The section renders a visible heading when `SectionHeading` is configured. +- If no visible heading exists, a visually hidden heading falls back to `ModuleTitle`. +- Every YouTube iframe receives a title based on the video title. +- Video embeds use a responsive aspect-ratio wrapper. +- Fallback thumbnail links receive accessible labels. + +## Out Of Scope + +- No custom DNN module. +- No dynamic routing. +- No heavy JavaScript filtering. +- No shared lookup data dependencies. diff --git a/Upendo-Video-Resource-List/data.json b/Upendo-Video-Resource-List/data.json new file mode 100644 index 0000000..57a6e80 --- /dev/null +++ b/Upendo-Video-Resource-List/data.json @@ -0,0 +1,57 @@ +{ + "ModuleTitle": "Upendo-Video-Resource-List", + "ModuleAnchor": "resources-videos", + "Eyebrow": "Video Resources", + "SectionHeading": "Upendo Ventures Video Library", + "SectionDescription": "

At Upendo Ventures, we help business owners like you turn frustrating, outdated websites into powerful business tools. Whether you're migrating from Wix, Squarespace, or WordPress—or just tired of your site not doing its job—we’re here to help you fix it, modernize it, and grow with it.

", + "Videos": [ + { + "Title": "Quick Technical Wins for ANY Small Business", + "Description": "

Is your small business making the impact it should be online? In this short and practical video, Upendo Ventures shares five simple but powerful tips that any business owner can start using today to look more professional, boost online visibility, and build customer trust.

", + "YouTubeUrl": "https://www.youtube.com/watch?v=UiuAZU8YG8U", + "ThumbnailImage": "https://img.youtube.com/vi/UiuAZU8YG8U/maxresdefault.jpg", + "ThumbnailAlt": "Quick Technical Wins for ANY Small Business video thumbnail", + "Category": "Upendo Ventures", + "Tags": "Small Business Tips, Digital Marketing, Upendo Ventures, Entrepreneur Tips, Business Growth, Online Branding, Password Manager, Marketing Simplified", + "CtaText": "Watch on YouTube", + "CtaUrl": "https://www.youtube.com/watch?v=UiuAZU8YG8U", + "CtaTarget": "_blank" + }, + { + "Title": "Modern Website Migrations for Small Businesses", + "Description": "

Tired of your outdated website? We make it easy to migrate from Wix, Squarespace, WordPress, and more—without the stress. Let’s build something better together.

", + "YouTubeUrl": "https://www.youtube.com/watch?v=UDhDlJ2AEgY", + "ThumbnailImage": "https://img.youtube.com/vi/UDhDlJ2AEgY/maxresdefault.jpg", + "ThumbnailAlt": "Modern Website Migrations for Small Businesses video thumbnail", + "Category": "Upendo Ventures", + "Tags": "Small Business, Website Migration, Digital Transformation, Upendo Ventures", + "CtaText": "Watch on YouTube", + "CtaUrl": "https://www.youtube.com/watch?v=UDhDlJ2AEgY", + "CtaTarget": "_blank" + }, + { + "Title": "How to Edit Page Meta Data in DNN CMS", + "Description": "

Learn how to easily edit and manage page-level SEO meta data in DNN CMS using a custom OpenContent template combined with the OpenContentHelper theme object. We’ll walk through how to update your page titles, descriptions, canonical URLs, Open Graph tags, and Twitter Cards — all without writing custom code. Perfect for developers, site owners, and marketers who want more control and better SEO performance from their DNN site.

", + "YouTubeUrl": "https://www.youtube.com/watch?v=8JtyEsgXiGo", + "ThumbnailImage": "https://img.youtube.com/vi/8JtyEsgXiGo/maxresdefault.jpg", + "ThumbnailAlt": "How to Edit Page Meta Data in DNN CMS video thumbnail", + "Category": "Technical Briefings", + "Tags": "DNN, SEO, OpenContent, Resources", + "CtaText": "Watch on YouTube", + "CtaUrl": "https://www.youtube.com/watch?v=8JtyEsgXiGo", + "CtaTarget": "_blank" + }, + { + "Title": "How to Upgrade DNN", + "Description": "

Upgrading DNN is far more simple than some people make it out to be. As long as you only install trusted extensions and only ones that you need, upgrades can literally be as easy as you see in this video.

", + "YouTubeUrl": "https://www.youtube.com/watch?v=bSgJ07AAT4s&t=48s", + "ThumbnailImage": "https://img.youtube.com/vi/bSgJ07AAT4s/maxresdefault.jpg", + "ThumbnailAlt": "How to Upgrade DNN video thumbnail", + "Category": "Technical Briefings", + "Tags": "DNN, Upgrades, Technical Briefing, Decision Support", + "CtaText": "Watch on YouTube", + "CtaUrl": "https://www.youtube.com/watch?v=bSgJ07AAT4s&t=48s", + "CtaTarget": "_blank" + } + ] +} diff --git a/Upendo-Video-Resource-List/options.json b/Upendo-Video-Resource-List/options.json new file mode 100644 index 0000000..b1e1055 --- /dev/null +++ b/Upendo-Video-Resource-List/options.json @@ -0,0 +1,92 @@ +{ + "fields": { + "ModuleTitle": { + "type": "text", + "helper": "Administrator-facing title for this OpenContent module. This helps identify the content block and is not displayed to visitors.", + "placeholder": "Video Resource List" + }, + "ModuleAnchor": { + "type": "text", + "helper": "Optional. Used to create a page anchor, such as resources-videos. Use letters, numbers, and hyphens only.", + "placeholder": "resources-videos", + "showMessages": false + }, + "Eyebrow": { + "type": "text", + "helper": "Optional. Small label shown above the section heading.", + "placeholder": "Video Resources" + }, + "SectionHeading": { + "type": "text", + "helper": "Optional. Main heading shown above the video list.", + "placeholder": "Video Library" + }, + "SectionDescription": { + "type": "textarea", + "helper": "Optional. Short introduction shown above the videos. HTML is allowed.", + "placeholder": "Watch brief explainers, product conversations, and field notes from the team." + }, + "Videos": { + "type": "accordion", + "helper": "Add each video resource here. Categories and tags are simple text fields so this template can remain independent.", + "items": { + "fields": { + "Title": { + "type": "text", + "helper": "Required. Public title for this video.", + "placeholder": "Platform Overview" + }, + "Description": { + "type": "textarea", + "helper": "Optional. Short description shown below the video.", + "placeholder": "A draft overview of how teams can evaluate and operationalize technical decisions." + }, + "YouTubeUrl": { + "type": "url", + "helper": "Optional. Paste a YouTube watch, youtu.be, shorts, or embed URL. The public embed uses youtube-nocookie.com when the video ID can be parsed.", + "placeholder": "https://www.youtube.com/watch?v=VIDEO_ID" + }, + "ThumbnailImage": { + "type": "image", + "helper": "Optional fallback image shown when no usable YouTube URL is configured. Recommended minimum: 1280 x 720 px.", + "uploadfolder": "Content/OpenContent/VideoResourceList/", + "typeahead": { + "Folder": "Content/OpenContent/VideoResourceList/" + } + }, + "ThumbnailAlt": { + "type": "text", + "helper": "Describe the thumbnail for assistive technology. Leave empty only if the image is decorative.", + "placeholder": "Video thumbnail" + }, + "Category": { + "type": "text", + "helper": "Optional. Simple label shown above the title and used for grouping when multiple categories exist.", + "placeholder": "Platform Overview" + }, + "Tags": { + "type": "text", + "helper": "Optional. Comma-separated tags shown as small badges.", + "placeholder": "AI strategy, evaluation, operations" + }, + "CtaText": { + "type": "text", + "helper": "Optional. Link text shown below the description. If empty, the YouTube URL is used as a source link only when a fallback card renders.", + "placeholder": "Watch on YouTube" + }, + "CtaUrl": { + "type": "url", + "helper": "Optional. Link URL for the CTA. If empty, the YouTube URL is used when CTA text is present.", + "placeholder": "https://www.youtube.com/watch?v=VIDEO_ID" + }, + "CtaTarget": { + "type": "select", + "helper": "Open the CTA in the same tab or a new tab. New tabs automatically receive noopener and noreferrer.", + "optionLabels": ["Same Tab", "New Tab"], + "removeDefaultNone": true + } + } + } + } + } +} diff --git a/Upendo-Video-Resource-List/schema.json b/Upendo-Video-Resource-List/schema.json new file mode 100644 index 0000000..5f4ae3b --- /dev/null +++ b/Upendo-Video-Resource-List/schema.json @@ -0,0 +1,80 @@ +{ + "type": "object", + "properties": { + "ModuleTitle": { + "type": "string", + "title": "Module Title" + }, + "ModuleAnchor": { + "type": "string", + "title": "Module Anchor", + "pattern": "^$|^[a-zA-Z0-9\\-]+$" + }, + "Eyebrow": { + "type": "string", + "title": "Eyebrow" + }, + "SectionHeading": { + "type": "string", + "title": "Section Heading" + }, + "SectionDescription": { + "type": "string", + "title": "Section Description" + }, + "Videos": { + "type": "array", + "title": "Videos", + "items": { + "type": "object", + "properties": { + "Title": { + "type": "string", + "title": "Title" + }, + "Description": { + "type": "string", + "title": "Description" + }, + "YouTubeUrl": { + "type": "string", + "title": "YouTube URL" + }, + "ThumbnailImage": { + "type": "string", + "title": "Thumbnail Image" + }, + "ThumbnailAlt": { + "type": "string", + "title": "Thumbnail Alt Text" + }, + "Category": { + "type": "string", + "title": "Category" + }, + "Tags": { + "type": "string", + "title": "Tags" + }, + "CtaText": { + "type": "string", + "title": "CTA Text" + }, + "CtaUrl": { + "type": "string", + "title": "CTA URL" + }, + "CtaTarget": { + "type": "string", + "title": "CTA Target", + "enum": ["_self", "_blank"], + "default": "_self" + } + }, + "required": [ + "Title" + ] + } + } + } +} diff --git a/Upendo-Video-Resource-List/template-data.json b/Upendo-Video-Resource-List/template-data.json new file mode 100644 index 0000000..2c53b05 --- /dev/null +++ b/Upendo-Video-Resource-List/template-data.json @@ -0,0 +1,12 @@ +{ + "BackgroundColorClass": "bg-body", + "ContainerClass": "container-xxl", + "MarginTop": "mt-5", + "MarginBottom": "mb-5", + "PaddingTop": "pt-5", + "PaddingBottom": "pb-5", + "DisplayMode": "grouped", + "CardsPerRow": "2", + "CompactSpacing": false, + "ShowBackToTop": true +} diff --git a/Upendo-Video-Resource-List/template-options.json b/Upendo-Video-Resource-List/template-options.json new file mode 100644 index 0000000..c6c88b2 --- /dev/null +++ b/Upendo-Video-Resource-List/template-options.json @@ -0,0 +1,105 @@ +{ + "fields": { + "BackgroundColorClass": { + "type": "select", + "helper": "Choose a Bootstrap background color class or one of the custom background utility classes used by the site theme.", + "optionLabels": [ + "None / Theme Default", + "Primary", + "Primary (theme)", + "Secondary", + "Secondary (theme)", + "Tertiary (theme)", + "Quaternary (theme)", + "H5 (theme)", + "H6 (theme)", + "Success", + "Danger", + "Warning", + "Info", + "Light", + "Dark", + "White", + "Body", + "Transparent", + "Primary Subtle", + "Secondary Subtle", + "Success Subtle", + "Danger Subtle", + "Warning Subtle", + "Info Subtle", + "Light Subtle", + "Dark Subtle", + "Off White", + "Warm White", + "Cream", + "Soft Beige", + "Sand", + "Light Sage", + "Sage", + "Pale Green", + "Mist", + "Soft Gray", + "Forest Green" + ], + "removeDefaultNone": true + }, + "ContainerClass": { + "type": "text", + "helper": "Wrapper class for the content width. Runtime accepts Bootstrap container classes: container, container-sm, container-md, container-lg, container-xl, container-xxl, or container-fluid.", + "placeholder": "container-xxl" + }, + "MarginTop": { + "title": "Margin (top)", + "type": "select", + "helper": "Optional. Controls spacing above this section.", + "optionLabels": ["None", "mt-0", "mt-1", "mt-2", "mt-3", "mt-4", "mt-5", "mt-auto"], + "removeDefaultNone": true + }, + "MarginBottom": { + "title": "Margin (bottom)", + "type": "select", + "helper": "Optional. Controls spacing below this section.", + "optionLabels": ["None", "mb-0", "mb-1", "mb-2", "mb-3", "mb-4", "mb-5", "mb-auto"], + "removeDefaultNone": true + }, + "PaddingTop": { + "title": "Padding (top)", + "type": "select", + "helper": "Optional. Controls inner spacing above the content.", + "optionLabels": ["None", "pt-0", "pt-1", "pt-2", "pt-3", "pt-4", "pt-5", "pt-auto"], + "removeDefaultNone": true + }, + "PaddingBottom": { + "title": "Padding (bottom)", + "type": "select", + "helper": "Optional. Controls inner spacing below the content.", + "optionLabels": ["None", "pb-0", "pb-1", "pb-2", "pb-3", "pb-4", "pb-5", "pb-auto"], + "removeDefaultNone": true + }, + "DisplayMode": { + "title": "Display mode", + "type": "select", + "helper": "Grouped sections keep category headings and category navigation. Unified grid renders all cards together in one compact grid.", + "optionLabels": ["Grouped sections", "Unified grid"], + "removeDefaultNone": true + }, + "CardsPerRow": { + "title": "Cards per row", + "type": "select", + "helper": "Controls the desktop card columns. Mobile remains one column.", + "optionLabels": ["2 cards", "3 cards"], + "removeDefaultNone": true + }, + "CompactSpacing": { + "title": "Compact spacing", + "type": "checkbox", + "helper": "Reduces gaps around the intro, category headings, and cards. Useful when the page feels too open." + }, + "ShowBackToTop": { + "title": "Show Back to top links", + "type": "checkbox", + "helper": "Shows a small Back to top link beside grouped category headings. Disable this when category sections are short." + } + } +} diff --git a/Upendo-Video-Resource-List/template-schema.json b/Upendo-Video-Resource-List/template-schema.json new file mode 100644 index 0000000..fa90774 --- /dev/null +++ b/Upendo-Video-Resource-List/template-schema.json @@ -0,0 +1,95 @@ +{ + "type": "object", + "properties": { + "BackgroundColorClass": { + "type": "string", + "title": "Background Color", + "enum": [ + "", + "bg-primary", + "bg-primary-scoped", + "bg-secondary", + "bg-secondary-scoped", + "bg-tertiary", + "bg-quaternary", + "bg-h5", + "bg-h6", + "bg-success", + "bg-danger", + "bg-warning", + "bg-info", + "bg-light", + "bg-dark", + "bg-white", + "bg-body", + "bg-transparent", + "bg-primary-subtle", + "bg-secondary-subtle", + "bg-success-subtle", + "bg-danger-subtle", + "bg-warning-subtle", + "bg-info-subtle", + "bg-light-subtle", + "bg-dark-subtle", + "bg-off-white", + "bg-warm-white", + "bg-cream", + "bg-soft-beige", + "bg-sand", + "bg-light-sage", + "bg-sage", + "bg-pale-green", + "bg-mist", + "bg-soft-gray", + "bg-forest-green" + ] + }, + "ContainerClass": { + "type": "string", + "title": "Container Class", + "default": "container-xxl" + }, + "MarginTop": { + "title": "Margin (top)", + "type": "string", + "enum": ["", "mt-0", "mt-1", "mt-2", "mt-3", "mt-4", "mt-5", "mt-auto"] + }, + "MarginBottom": { + "title": "Margin (bottom)", + "type": "string", + "enum": ["", "mb-0", "mb-1", "mb-2", "mb-3", "mb-4", "mb-5", "mb-auto"] + }, + "PaddingTop": { + "title": "Padding (top)", + "type": "string", + "enum": ["", "pt-0", "pt-1", "pt-2", "pt-3", "pt-4", "pt-5", "pt-auto"] + }, + "PaddingBottom": { + "title": "Padding (bottom)", + "type": "string", + "enum": ["", "pb-0", "pb-1", "pb-2", "pb-3", "pb-4", "pb-5", "pb-auto"] + }, + "DisplayMode": { + "title": "Display Mode", + "type": "string", + "enum": ["grouped", "grid"], + "default": "grouped" + }, + "CardsPerRow": { + "title": "Cards Per Row", + "type": "string", + "enum": ["2", "3"], + "default": "2" + }, + "CompactSpacing": { + "title": "Compact Spacing", + "type": "boolean", + "default": false + }, + "ShowBackToTop": { + "title": "Show Back To Top Links", + "type": "boolean", + "default": true + } + } +} diff --git a/Upendo-Video-Resource-List/template.cshtml b/Upendo-Video-Resource-List/template.cshtml new file mode 100644 index 0000000..fe27cea --- /dev/null +++ b/Upendo-Video-Resource-List/template.cshtml @@ -0,0 +1,800 @@ +@using System +@using System.Collections.Generic +@using System.Text +@using System.Web +@using Upendo.Libraries.RunsOnUpendoCore.Helpers +@inherits Satrabel.OpenContent.Components.OpenContentWebPage + +@{ + TemplateHelper.HideAdminBorder(Model.Context.ModuleId, Model.Context.TabId); + RegisterStyleSheet("template.css"); + + var output = new StringBuilder(); + var moduleTitle = Model.ModuleTitle == null ? string.Empty : Convert.ToString(Model.ModuleTitle).Trim(); + var moduleAnchor = Model.ModuleAnchor == null ? string.Empty : Convert.ToString(Model.ModuleAnchor).Trim(); + var eyebrow = Model.Eyebrow == null ? string.Empty : Convert.ToString(Model.Eyebrow).Trim(); + var sectionHeading = Model.SectionHeading == null ? string.Empty : Convert.ToString(Model.SectionHeading).Trim(); + var sectionDescription = Model.SectionDescription == null ? string.Empty : Convert.ToString(Model.SectionDescription).Trim(); + + var backgroundColorClass = "bg-body"; + var containerClass = "container-xxl"; + var marginTop = string.Empty; + var marginBottom = string.Empty; + var paddingTop = string.Empty; + var paddingBottom = string.Empty; + var displayMode = "grouped"; + var cardsPerRow = "2"; + var compactSpacing = false; + var showBackToTop = false; + + if (Model.Settings != null) + { + var settingValue = Model.Settings.BackgroundColorClass == null ? string.Empty : Convert.ToString(Model.Settings.BackgroundColorClass).Trim(); + var allowedBackgrounds = new List { "", "bg-primary", "bg-primary-scoped", "bg-secondary", "bg-secondary-scoped", "bg-tertiary", "bg-quaternary", "bg-h5", "bg-h6", "bg-success", "bg-danger", "bg-warning", "bg-info", "bg-light", "bg-dark", "bg-white", "bg-body", "bg-transparent", "bg-primary-subtle", "bg-secondary-subtle", "bg-success-subtle", "bg-danger-subtle", "bg-warning-subtle", "bg-info-subtle", "bg-light-subtle", "bg-dark-subtle", "bg-off-white", "bg-warm-white", "bg-cream", "bg-soft-beige", "bg-sand", "bg-light-sage", "bg-sage", "bg-pale-green", "bg-mist", "bg-soft-gray", "bg-forest-green" }; + for (var i = 0; i < allowedBackgrounds.Count; i++) + { + if (string.Equals(settingValue, allowedBackgrounds[i], StringComparison.OrdinalIgnoreCase)) + { + backgroundColorClass = allowedBackgrounds[i]; + } + } + + settingValue = Model.Settings.ContainerClass == null ? string.Empty : Convert.ToString(Model.Settings.ContainerClass).Trim(); + var allowedContainers = new List { "container", "container-sm", "container-md", "container-lg", "container-xl", "container-xxl", "container-fluid" }; + for (var i = 0; i < allowedContainers.Count; i++) + { + if (string.Equals(settingValue, allowedContainers[i], StringComparison.OrdinalIgnoreCase)) + { + containerClass = allowedContainers[i]; + } + } + + settingValue = Model.Settings.MarginTop == null ? string.Empty : Convert.ToString(Model.Settings.MarginTop).Trim(); + var allowedMarginTop = new List { "", "mt-0", "mt-1", "mt-2", "mt-3", "mt-4", "mt-5", "mt-auto" }; + for (var i = 0; i < allowedMarginTop.Count; i++) + { + if (string.Equals(settingValue, allowedMarginTop[i], StringComparison.OrdinalIgnoreCase)) + { + marginTop = allowedMarginTop[i]; + } + } + + settingValue = Model.Settings.MarginBottom == null ? string.Empty : Convert.ToString(Model.Settings.MarginBottom).Trim(); + var allowedMarginBottom = new List { "", "mb-0", "mb-1", "mb-2", "mb-3", "mb-4", "mb-5", "mb-auto" }; + for (var i = 0; i < allowedMarginBottom.Count; i++) + { + if (string.Equals(settingValue, allowedMarginBottom[i], StringComparison.OrdinalIgnoreCase)) + { + marginBottom = allowedMarginBottom[i]; + } + } + + settingValue = Model.Settings.PaddingTop == null ? string.Empty : Convert.ToString(Model.Settings.PaddingTop).Trim(); + var allowedPaddingTop = new List { "", "pt-0", "pt-1", "pt-2", "pt-3", "pt-4", "pt-5", "pt-auto" }; + for (var i = 0; i < allowedPaddingTop.Count; i++) + { + if (string.Equals(settingValue, allowedPaddingTop[i], StringComparison.OrdinalIgnoreCase)) + { + paddingTop = allowedPaddingTop[i]; + } + } + + settingValue = Model.Settings.PaddingBottom == null ? string.Empty : Convert.ToString(Model.Settings.PaddingBottom).Trim(); + var allowedPaddingBottom = new List { "", "pb-0", "pb-1", "pb-2", "pb-3", "pb-4", "pb-5", "pb-auto" }; + for (var i = 0; i < allowedPaddingBottom.Count; i++) + { + if (string.Equals(settingValue, allowedPaddingBottom[i], StringComparison.OrdinalIgnoreCase)) + { + paddingBottom = allowedPaddingBottom[i]; + } + } + + settingValue = Model.Settings.DisplayMode == null ? string.Empty : Convert.ToString(Model.Settings.DisplayMode).Trim(); + var allowedDisplayModes = new List { "grouped", "grid" }; + for (var i = 0; i < allowedDisplayModes.Count; i++) + { + if (string.Equals(settingValue, allowedDisplayModes[i], StringComparison.OrdinalIgnoreCase)) + { + displayMode = allowedDisplayModes[i]; + } + } + + settingValue = Model.Settings.CardsPerRow == null ? string.Empty : Convert.ToString(Model.Settings.CardsPerRow).Trim(); + var allowedCardsPerRow = new List { "2", "3" }; + for (var i = 0; i < allowedCardsPerRow.Count; i++) + { + if (string.Equals(settingValue, allowedCardsPerRow[i], StringComparison.OrdinalIgnoreCase)) + { + cardsPerRow = allowedCardsPerRow[i]; + } + } + + settingValue = Model.Settings.CompactSpacing == null ? string.Empty : Convert.ToString(Model.Settings.CompactSpacing).Trim(); + compactSpacing = string.Equals(settingValue, "true", StringComparison.OrdinalIgnoreCase) || string.Equals(settingValue, "1", StringComparison.OrdinalIgnoreCase) || string.Equals(settingValue, "yes", StringComparison.OrdinalIgnoreCase); + + settingValue = Model.Settings.ShowBackToTop == null ? string.Empty : Convert.ToString(Model.Settings.ShowBackToTop).Trim(); + if (string.Equals(settingValue, "true", StringComparison.OrdinalIgnoreCase) || string.Equals(settingValue, "1", StringComparison.OrdinalIgnoreCase) || string.Equals(settingValue, "yes", StringComparison.OrdinalIgnoreCase)) + { + showBackToTop = true; + } + } + + var videos = new List(); + var categories = new List(); + + if (Model.Videos != null) + { + foreach (var video in Model.Videos) + { + videos.Add(video); + + var itemCategory = video.Category == null ? string.Empty : Convert.ToString(video.Category).Trim(); + if (string.IsNullOrWhiteSpace(itemCategory)) + { + itemCategory = "Featured Videos"; + } + + var categoryExists = false; + for (var i = 0; i < categories.Count; i++) + { + if (string.Equals(categories[i], itemCategory, StringComparison.OrdinalIgnoreCase)) + { + categoryExists = true; + } + } + + if (!categoryExists) + { + categories.Add(itemCategory); + } + } + } + + var sectionId = !string.IsNullOrWhiteSpace(moduleAnchor) ? moduleAnchor : "oc-video-resource-list-" + Model.Context.ModuleId; + var headingId = "oc-video-resource-list-heading-" + Model.Context.ModuleId; + var sectionLabel = !string.IsNullOrWhiteSpace(sectionHeading) ? sectionHeading : (!string.IsNullOrWhiteSpace(moduleTitle) ? moduleTitle : "Video resources"); + var sectionClass = "oc-video-resource-list oc-video-resource-list--" + displayMode + " oc-video-resource-list--cols-" + cardsPerRow + " " + backgroundColorClass; + var columnClass = string.Equals(cardsPerRow, "3", StringComparison.OrdinalIgnoreCase) ? "col-12 col-lg-4" : "col-12 col-lg-6"; + var gridClass = compactSpacing ? "row g-3 oc-video-resource-list__grid" : "row g-4 oc-video-resource-list__grid"; + + if (!string.IsNullOrWhiteSpace(marginTop)) { sectionClass += " " + marginTop; } + if (!string.IsNullOrWhiteSpace(marginBottom)) { sectionClass += " " + marginBottom; } + if (!string.IsNullOrWhiteSpace(paddingTop)) { sectionClass += " " + paddingTop; } + if (!string.IsNullOrWhiteSpace(paddingBottom)) { sectionClass += " " + paddingBottom; } + if (compactSpacing) { sectionClass += " oc-video-resource-list--compact"; } + + var isEditMode = false; + try + { + isEditMode = Model.Context.IsEditMode; + } + catch + { + try + { + isEditMode = Model.Context.IsEditable; + } + catch + { + } + } + + if (videos.Count > 0) + { + output.Append("
"); + output.Append("
"); + + if (!string.IsNullOrWhiteSpace(eyebrow)) + { + output.Append("

"); + output.Append(HttpUtility.HtmlEncode(eyebrow)); + output.Append("

"); + } + + if (!string.IsNullOrWhiteSpace(sectionHeading)) + { + output.Append("

"); + output.Append(HttpUtility.HtmlEncode(sectionHeading)); + output.Append("

"); + } + else + { + output.Append("

"); + output.Append(HttpUtility.HtmlEncode(sectionLabel)); + output.Append("

"); + } + + if (!string.IsNullOrWhiteSpace(sectionDescription)) + { + output.Append("
"); + output.Append(sectionDescription); + output.Append("
"); + } + + output.Append("
"); + + if (categories.Count > 1 && string.Equals(displayMode, "grouped", StringComparison.OrdinalIgnoreCase)) + { + output.Append(""); + } + + if (string.Equals(displayMode, "grid", StringComparison.OrdinalIgnoreCase)) + { + output.Append("
"); + + for (var videoIndex = 0; videoIndex < videos.Count; videoIndex++) + { + var video = videos[videoIndex]; + var category = video.Category == null ? string.Empty : Convert.ToString(video.Category).Trim(); + if (string.IsNullOrWhiteSpace(category)) + { + category = "Featured Videos"; + } + + var title = video.Title == null ? string.Empty : Convert.ToString(video.Title).Trim(); + if (string.IsNullOrWhiteSpace(title)) + { + title = "Video"; + } + + var description = video.Description == null ? string.Empty : Convert.ToString(video.Description).Trim(); + var youtubeUrl = video.YouTubeUrl == null ? string.Empty : Convert.ToString(video.YouTubeUrl).Trim(); + var safeYoutubeUrl = string.Empty; + if (!string.IsNullOrWhiteSpace(youtubeUrl) && !youtubeUrl.Contains("<") && !youtubeUrl.Contains(">") && !youtubeUrl.StartsWith("//", StringComparison.Ordinal)) + { + if (youtubeUrl.StartsWith("#", StringComparison.Ordinal) || youtubeUrl.StartsWith("/", StringComparison.Ordinal)) + { + safeYoutubeUrl = youtubeUrl; + } + else + { + Uri parsedSafeUrl; + if (Uri.TryCreate(youtubeUrl, UriKind.Absolute, out parsedSafeUrl) && + (parsedSafeUrl.Scheme == Uri.UriSchemeHttp || parsedSafeUrl.Scheme == Uri.UriSchemeHttps || parsedSafeUrl.Scheme == Uri.UriSchemeMailto || parsedSafeUrl.Scheme == "tel")) + { + safeYoutubeUrl = youtubeUrl; + } + } + } + + var youtubeVideoId = string.Empty; + if (!string.IsNullOrWhiteSpace(youtubeUrl) && !youtubeUrl.Contains("<") && !youtubeUrl.Contains(">")) + { + Uri parsedYouTubeUrl; + if (Uri.TryCreate(youtubeUrl, UriKind.Absolute, out parsedYouTubeUrl) && + parsedYouTubeUrl != null && + (string.Equals(parsedYouTubeUrl.Scheme, "http", StringComparison.OrdinalIgnoreCase) || string.Equals(parsedYouTubeUrl.Scheme, "https", StringComparison.OrdinalIgnoreCase))) + { + var host = parsedYouTubeUrl.Host == null ? string.Empty : parsedYouTubeUrl.Host.ToLowerInvariant(); + var path = parsedYouTubeUrl.AbsolutePath == null ? string.Empty : parsedYouTubeUrl.AbsolutePath.Trim('/'); + var candidateVideoId = string.Empty; + + if (host == "youtu.be") + { + var pathParts = path.Split('/'); + if (pathParts.Length > 0) + { + candidateVideoId = pathParts[0]; + } + } + else if (host == "youtube.com" || host == "www.youtube.com" || host == "m.youtube.com" || host == "music.youtube.com" || host == "www.youtube-nocookie.com" || host == "youtube-nocookie.com") + { + var queryValues = HttpUtility.ParseQueryString(parsedYouTubeUrl.Query); + candidateVideoId = queryValues["v"]; + + if (string.IsNullOrWhiteSpace(candidateVideoId)) + { + var pathParts = path.Split('/'); + if (pathParts.Length >= 2 && + (string.Equals(pathParts[0], "embed", StringComparison.OrdinalIgnoreCase) || string.Equals(pathParts[0], "shorts", StringComparison.OrdinalIgnoreCase))) + { + candidateVideoId = pathParts[1]; + } + } + } + + if (!string.IsNullOrWhiteSpace(candidateVideoId)) + { + for (var i = 0; i < candidateVideoId.Length; i++) + { + var c = candidateVideoId[i]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-') + { + youtubeVideoId += c; + } + } + } + } + } + + var embedUrl = !string.IsNullOrWhiteSpace(youtubeVideoId) ? "https://www.youtube-nocookie.com/embed/" + youtubeVideoId + "?rel=0&modestbranding=1&playsinline=1" : string.Empty; + var thumbnailImage = string.Empty; + if (video.ThumbnailImage != null) + { + try + { + if (video.ThumbnailImage.Url != null) + { + thumbnailImage = Convert.ToString(video.ThumbnailImage.Url).Trim(); + } + } + catch + { + } + + if (string.IsNullOrWhiteSpace(thumbnailImage)) + { + thumbnailImage = Convert.ToString(video.ThumbnailImage).Trim(); + } + } + + var thumbnailAlt = video.ThumbnailAlt == null ? string.Empty : Convert.ToString(video.ThumbnailAlt).Trim(); + if (string.IsNullOrWhiteSpace(thumbnailAlt)) + { + thumbnailAlt = title; + } + + var ctaText = video.CtaText == null ? string.Empty : Convert.ToString(video.CtaText).Trim(); + var rawCtaUrl = video.CtaUrl == null ? string.Empty : Convert.ToString(video.CtaUrl).Trim(); + if (string.IsNullOrWhiteSpace(rawCtaUrl)) + { + rawCtaUrl = youtubeUrl; + } + + var ctaUrl = string.Empty; + if (!string.IsNullOrWhiteSpace(rawCtaUrl) && !rawCtaUrl.Contains("<") && !rawCtaUrl.Contains(">") && !rawCtaUrl.StartsWith("//", StringComparison.Ordinal)) + { + if (rawCtaUrl.StartsWith("#", StringComparison.Ordinal) || rawCtaUrl.StartsWith("/", StringComparison.Ordinal)) + { + ctaUrl = rawCtaUrl; + } + else + { + Uri parsedCtaUrl; + if (Uri.TryCreate(rawCtaUrl, UriKind.Absolute, out parsedCtaUrl) && + (parsedCtaUrl.Scheme == Uri.UriSchemeHttp || parsedCtaUrl.Scheme == Uri.UriSchemeHttps || parsedCtaUrl.Scheme == Uri.UriSchemeMailto || parsedCtaUrl.Scheme == "tel")) + { + ctaUrl = rawCtaUrl; + } + } + } + + var ctaTarget = video.CtaTarget == null ? string.Empty : Convert.ToString(video.CtaTarget).Trim(); + if (!string.Equals(ctaTarget, "_blank", StringComparison.OrdinalIgnoreCase)) + { + ctaTarget = "_self"; + } + + var opensNewWindow = string.Equals(ctaTarget, "_blank", StringComparison.OrdinalIgnoreCase); + var targetAttributes = opensNewWindow ? " target=\"_blank\" rel=\"noopener noreferrer\"" : string.Empty; + var hasEmbed = !string.IsNullOrWhiteSpace(embedUrl); + var hasThumbnail = !string.IsNullOrWhiteSpace(thumbnailImage); + var hasCta = !string.IsNullOrWhiteSpace(ctaText) && !string.IsNullOrWhiteSpace(ctaUrl); + var tags = new List(); + var tagsText = video.Tags == null ? string.Empty : Convert.ToString(video.Tags).Trim(); + if (!string.IsNullOrWhiteSpace(tagsText)) + { + var tagParts = tagsText.Split(','); + for (var i = 0; i < tagParts.Length; i++) + { + var tag = tagParts[i].Trim(); + if (!string.IsNullOrWhiteSpace(tag)) + { + tags.Add(tag); + } + } + } + + output.Append("
"); + + if (hasEmbed) + { + output.Append(""); + } + else if (hasThumbnail) + { + if (!string.IsNullOrWhiteSpace(safeYoutubeUrl)) + { + output.Append("\"");"); + } + else + { + output.Append("\"");"); + } + } + else if (isEditMode) + { + output.Append("
Add a valid YouTube URL or fallback thumbnail for this video.
"); + } + + output.Append("
"); + + if (!string.IsNullOrWhiteSpace(category)) + { + output.Append("

"); + output.Append(HttpUtility.HtmlEncode(category)); + output.Append("

"); + } + + output.Append("

"); + output.Append(HttpUtility.HtmlEncode(title)); + output.Append("

"); + + if (!string.IsNullOrWhiteSpace(description)) + { + output.Append("
"); + output.Append(description); + output.Append("
"); + } + + if (tags.Count > 0) + { + output.Append("
    "); + for (var i = 0; i < tags.Count; i++) + { + output.Append("
  • "); + output.Append(HttpUtility.HtmlEncode(tags[i])); + output.Append("
  • "); + } + output.Append("
"); + } + + if (hasCta) + { + output.Append(""); + output.Append(HttpUtility.HtmlEncode(ctaText)); + output.Append(""); + } + + output.Append("
"); + } + + output.Append("
"); + } + + for (var categoryIndex = 0; categoryIndex < categories.Count && string.Equals(displayMode, "grouped", StringComparison.OrdinalIgnoreCase); categoryIndex++) + { + var currentCategory = categories[categoryIndex]; + var categoryId = "oc-video-resource-category-" + Model.Context.ModuleId + "-" + (categoryIndex + 1); + + output.Append("
"); + + if (categories.Count > 1) + { + output.Append("

"); + output.Append(HttpUtility.HtmlEncode(currentCategory)); + output.Append("

"); + if (showBackToTop && categoryIndex > 0) + { + output.Append("Back to top"); + } + output.Append("
"); + } + + output.Append("
"); + + foreach (var video in videos) + { + var category = video.Category == null ? string.Empty : Convert.ToString(video.Category).Trim(); + if (string.IsNullOrWhiteSpace(category)) + { + category = "Featured Videos"; + } + + if (string.Equals(category, currentCategory, StringComparison.OrdinalIgnoreCase)) + { + var title = video.Title == null ? string.Empty : Convert.ToString(video.Title).Trim(); + if (string.IsNullOrWhiteSpace(title)) + { + title = "Video"; + } + + var description = video.Description == null ? string.Empty : Convert.ToString(video.Description).Trim(); + var youtubeUrl = video.YouTubeUrl == null ? string.Empty : Convert.ToString(video.YouTubeUrl).Trim(); + var safeYoutubeUrl = string.Empty; + if (!string.IsNullOrWhiteSpace(youtubeUrl) && !youtubeUrl.Contains("<") && !youtubeUrl.Contains(">") && !youtubeUrl.StartsWith("//", StringComparison.Ordinal)) + { + if (youtubeUrl.StartsWith("#", StringComparison.Ordinal) || youtubeUrl.StartsWith("/", StringComparison.Ordinal)) + { + safeYoutubeUrl = youtubeUrl; + } + else + { + Uri parsedSafeUrl; + if (Uri.TryCreate(youtubeUrl, UriKind.Absolute, out parsedSafeUrl) && + (parsedSafeUrl.Scheme == Uri.UriSchemeHttp || parsedSafeUrl.Scheme == Uri.UriSchemeHttps || parsedSafeUrl.Scheme == Uri.UriSchemeMailto || parsedSafeUrl.Scheme == "tel")) + { + safeYoutubeUrl = youtubeUrl; + } + } + } + + var youtubeVideoId = string.Empty; + if (!string.IsNullOrWhiteSpace(youtubeUrl) && !youtubeUrl.Contains("<") && !youtubeUrl.Contains(">")) + { + Uri parsedYouTubeUrl; + if (Uri.TryCreate(youtubeUrl, UriKind.Absolute, out parsedYouTubeUrl) && + parsedYouTubeUrl != null && + (string.Equals(parsedYouTubeUrl.Scheme, "http", StringComparison.OrdinalIgnoreCase) || string.Equals(parsedYouTubeUrl.Scheme, "https", StringComparison.OrdinalIgnoreCase))) + { + var host = parsedYouTubeUrl.Host == null ? string.Empty : parsedYouTubeUrl.Host.ToLowerInvariant(); + var path = parsedYouTubeUrl.AbsolutePath == null ? string.Empty : parsedYouTubeUrl.AbsolutePath.Trim('/'); + var candidateVideoId = string.Empty; + + if (host == "youtu.be") + { + var pathParts = path.Split('/'); + if (pathParts.Length > 0) + { + candidateVideoId = pathParts[0]; + } + } + else if (host == "youtube.com" || host == "www.youtube.com" || host == "m.youtube.com" || host == "music.youtube.com" || host == "www.youtube-nocookie.com" || host == "youtube-nocookie.com") + { + var queryValues = HttpUtility.ParseQueryString(parsedYouTubeUrl.Query); + candidateVideoId = queryValues["v"]; + + if (string.IsNullOrWhiteSpace(candidateVideoId)) + { + var pathParts = path.Split('/'); + if (pathParts.Length >= 2 && + (string.Equals(pathParts[0], "embed", StringComparison.OrdinalIgnoreCase) || string.Equals(pathParts[0], "shorts", StringComparison.OrdinalIgnoreCase))) + { + candidateVideoId = pathParts[1]; + } + } + } + + if (!string.IsNullOrWhiteSpace(candidateVideoId)) + { + for (var i = 0; i < candidateVideoId.Length; i++) + { + var c = candidateVideoId[i]; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-') + { + youtubeVideoId += c; + } + } + } + } + } + + var embedUrl = !string.IsNullOrWhiteSpace(youtubeVideoId) ? "https://www.youtube-nocookie.com/embed/" + youtubeVideoId + "?rel=0&modestbranding=1&playsinline=1" : string.Empty; + var thumbnailImage = string.Empty; + if (video.ThumbnailImage != null) + { + try + { + if (video.ThumbnailImage.Url != null) + { + thumbnailImage = Convert.ToString(video.ThumbnailImage.Url).Trim(); + } + } + catch + { + } + + if (string.IsNullOrWhiteSpace(thumbnailImage)) + { + thumbnailImage = Convert.ToString(video.ThumbnailImage).Trim(); + } + } + + var thumbnailAlt = video.ThumbnailAlt == null ? string.Empty : Convert.ToString(video.ThumbnailAlt).Trim(); + if (string.IsNullOrWhiteSpace(thumbnailAlt)) + { + thumbnailAlt = title; + } + + var ctaText = video.CtaText == null ? string.Empty : Convert.ToString(video.CtaText).Trim(); + var rawCtaUrl = video.CtaUrl == null ? string.Empty : Convert.ToString(video.CtaUrl).Trim(); + if (string.IsNullOrWhiteSpace(rawCtaUrl)) + { + rawCtaUrl = youtubeUrl; + } + + var ctaUrl = string.Empty; + if (!string.IsNullOrWhiteSpace(rawCtaUrl) && !rawCtaUrl.Contains("<") && !rawCtaUrl.Contains(">") && !rawCtaUrl.StartsWith("//", StringComparison.Ordinal)) + { + if (rawCtaUrl.StartsWith("#", StringComparison.Ordinal) || rawCtaUrl.StartsWith("/", StringComparison.Ordinal)) + { + ctaUrl = rawCtaUrl; + } + else + { + Uri parsedCtaUrl; + if (Uri.TryCreate(rawCtaUrl, UriKind.Absolute, out parsedCtaUrl) && + (parsedCtaUrl.Scheme == Uri.UriSchemeHttp || parsedCtaUrl.Scheme == Uri.UriSchemeHttps || parsedCtaUrl.Scheme == Uri.UriSchemeMailto || parsedCtaUrl.Scheme == "tel")) + { + ctaUrl = rawCtaUrl; + } + } + } + + var ctaTarget = video.CtaTarget == null ? string.Empty : Convert.ToString(video.CtaTarget).Trim(); + if (!string.Equals(ctaTarget, "_blank", StringComparison.OrdinalIgnoreCase)) + { + ctaTarget = "_self"; + } + + var opensNewWindow = string.Equals(ctaTarget, "_blank", StringComparison.OrdinalIgnoreCase); + var targetAttributes = opensNewWindow ? " target=\"_blank\" rel=\"noopener noreferrer\"" : string.Empty; + var hasEmbed = !string.IsNullOrWhiteSpace(embedUrl); + var hasThumbnail = !string.IsNullOrWhiteSpace(thumbnailImage); + var hasCta = !string.IsNullOrWhiteSpace(ctaText) && !string.IsNullOrWhiteSpace(ctaUrl); + var tags = new List(); + var tagsText = video.Tags == null ? string.Empty : Convert.ToString(video.Tags).Trim(); + if (!string.IsNullOrWhiteSpace(tagsText)) + { + var tagParts = tagsText.Split(','); + for (var i = 0; i < tagParts.Length; i++) + { + var tag = tagParts[i].Trim(); + if (!string.IsNullOrWhiteSpace(tag)) + { + tags.Add(tag); + } + } + } + + output.Append("
"); + + if (hasEmbed) + { + output.Append(""); + } + else if (hasThumbnail) + { + if (!string.IsNullOrWhiteSpace(safeYoutubeUrl)) + { + output.Append("\"");"); + } + else + { + output.Append("\"");"); + } + } + else if (isEditMode) + { + output.Append("
Add a valid YouTube URL or fallback thumbnail for this video.
"); + } + + output.Append("
"); + + if (!string.IsNullOrWhiteSpace(category)) + { + output.Append("

"); + output.Append(HttpUtility.HtmlEncode(category)); + output.Append("

"); + } + + output.Append("

"); + output.Append(HttpUtility.HtmlEncode(title)); + output.Append("

"); + + if (!string.IsNullOrWhiteSpace(description)) + { + output.Append("
"); + output.Append(description); + output.Append("
"); + } + + if (tags.Count > 0) + { + output.Append("
    "); + for (var i = 0; i < tags.Count; i++) + { + output.Append("
  • "); + output.Append(HttpUtility.HtmlEncode(tags[i])); + output.Append("
  • "); + } + output.Append("
"); + } + + if (hasCta) + { + output.Append(""); + output.Append(HttpUtility.HtmlEncode(ctaText)); + output.Append(""); + } + + output.Append("
"); + } + } + + output.Append("
"); + } + + output.Append("
"); + } + else if (isEditMode) + { + output.Append("
Add at least one video resource to render the video list.
"); + } + + var renderedHtml = output.ToString(); +} + +@Html.Raw(renderedHtml) diff --git a/Upendo-Video-Resource-List/template.css b/Upendo-Video-Resource-List/template.css new file mode 100644 index 0000000..75b3902 --- /dev/null +++ b/Upendo-Video-Resource-List/template.css @@ -0,0 +1,391 @@ +.oc-video-resource-list { + --oc-video-resource-primary: var(--bs-primary, #01233f); + --oc-video-resource-primary-hover: var(--bs-secondary, #4a688c); + --oc-video-resource-primary-rgb: var(--bs-primary-rgb, 1, 35, 63); + --oc-video-resource-soft: var(--bs-light, #ebeef3); + width: 100%; +} + +.oc-video-resource-list__eyebrow { + color: var(--oc-video-resource-primary); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.12em; + line-height: 1.2; + text-transform: uppercase; +} + +.oc-video-resource-list__heading { + color: inherit; +} + +.oc-video-resource-list__description { + max-width: 44rem; +} + +.oc-video-resource-list__intro { + margin-bottom: 1.5rem; +} + +.oc-video-resource-list__description p:last-child, +.oc-video-resource-list__card-description p:last-child { + margin-bottom: 0; +} + +.oc-video-resource-list .oc-video-resource-list__nav-link, +.oc-video-resource-list .oc-video-resource-list__nav-link:link, +.oc-video-resource-list .oc-video-resource-list__nav-link:visited { + background-color: var(--oc-video-resource-soft); + border: 1px solid rgba(var(--oc-video-resource-primary-rgb), 0.35); + color: var(--oc-video-resource-primary); + font-weight: 600; + text-decoration: none; + transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease; +} + +.oc-video-resource-list .oc-video-resource-list__nav-link:hover, +.oc-video-resource-list .oc-video-resource-list__nav-link:focus, +.oc-video-resource-list .oc-video-resource-list__nav-link:active, +.oc-video-resource-list .oc-video-resource-list__nav-link:focus-visible { + background-color: var(--oc-video-resource-primary); + border-color: var(--oc-video-resource-primary); + color: #ffffff; + text-decoration: none; + transform: translateY(-1px); +} + +.oc-video-resource-list .oc-video-resource-list__nav-link:focus-visible, +.oc-video-resource-list .oc-video-resource-list__button:focus-visible, +.oc-video-resource-list__thumbnail-link:focus-visible { + outline: 2px solid rgba(var(--oc-video-resource-primary-rgb), 0.35); + outline-offset: 3px; +} + +.oc-video-resource-list__category-anchor { + position: relative; + top: -6rem; + visibility: hidden; +} + +.oc-video-resource-list__category-heading-wrap { + margin-bottom: 1.25rem; + margin-top: 0 !important; +} + +.oc-video-resource-list__category-heading-wrap > * { + margin-top: 0 !important; +} + +.oc-video-resource-list__nav { + margin-bottom: 1.5rem; +} + +.oc-video-resource-list__nav .nav-item, +.oc-video-resource-list__nav .nav-item + .nav-item { + margin-top: 0 !important; +} + +.oc-video-resource-list__category-heading { + color: inherit; + margin-top: 1.25rem !important; +} + +.oc-video-resource-list__back-link, +.oc-video-resource-list__back-link:link, +.oc-video-resource-list__back-link:visited { + align-items: center; + background-color: var(--oc-video-resource-primary); + border: 1px solid var(--oc-video-resource-primary); + border-radius: 999px; + color: #ffffff; + display: inline-flex; + font-size: 0.76rem; + font-weight: 700; + line-height: 1.2; + margin-left: auto; + margin-top: 0 !important; + padding: 0.38rem 0.78rem; + text-decoration: none; + transition: background-color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; + white-space: nowrap; +} + +.oc-video-resource-list__back-link:hover, +.oc-video-resource-list__back-link:focus, +.oc-video-resource-list__back-link:active, +.oc-video-resource-list__back-link:focus-visible { + background-color: var(--oc-video-resource-primary-hover); + border-color: var(--oc-video-resource-primary-hover); + color: #ffffff; + text-decoration: none !important; + transform: translateY(-1px); +} + +.oc-video-resource-list__back-link:focus-visible { + box-shadow: 0 0 0 0.2rem rgba(var(--oc-video-resource-primary-rgb), 0.22); + outline: 2px solid rgba(var(--oc-video-resource-primary-rgb), 0.35); + outline-offset: 3px; +} + +.oc-video-resource-list__back-link i { + color: inherit; + font-size: 0.7rem; + margin-top: 0 !important; +} + +.oc-video-resource-list__grid + .oc-video-resource-list__category-anchor { + display: block; + margin-top: 3.25rem; +} + +.oc-video-resource-list__grid + .oc-video-resource-list__category-anchor + .oc-video-resource-list__category-heading-wrap { + margin-top: 0 !important; +} + +.oc-video-resource-list__card { + background: rgba(255, 255, 255, 0.94); + border: 1px solid rgba(var(--oc-video-resource-primary-rgb), 0.08); + border-radius: 1rem; + box-shadow: 0 0.55rem 1.45rem rgba(20, 40, 30, 0.065); + display: flex; + flex-direction: column; + overflow: hidden; + transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease; +} + +.oc-video-resource-list__card:hover { + border-color: rgba(var(--oc-video-resource-primary-rgb), 0.18); + box-shadow: 0 1rem 2.5rem rgba(20, 40, 30, 0.1); + transform: translateY(-2px); +} + +.oc-video-resource-list__media { + aspect-ratio: 16 / 9; + background: linear-gradient(135deg, rgba(var(--oc-video-resource-primary-rgb), 0.1), rgba(var(--oc-video-resource-primary-rgb), 0.24)); + overflow: hidden; + position: relative; + width: 100%; +} + +.oc-video-resource-list__iframe, +.oc-video-resource-list__thumbnail, +.oc-video-resource-list__thumbnail-link { + display: block; + height: 100%; + width: 100%; +} + +.oc-video-resource-list__iframe { + border: 0; +} + +.oc-video-resource-list__thumbnail { + object-fit: cover; +} + +.oc-video-resource-list__thumbnail-link { + color: #ffffff; + position: relative; + text-decoration: none; +} + +.oc-video-resource-list__thumbnail-link::after { + background: linear-gradient(180deg, rgba(0, 0, 0, 0.04), rgba(0, 0, 0, 0.28)); + content: ""; + inset: 0; + position: absolute; +} + +.oc-video-resource-list__play { + align-items: center; + background: rgba(var(--oc-video-resource-primary-rgb), 0.92); + border-radius: 999px; + color: #ffffff; + display: inline-flex; + height: 3.25rem; + justify-content: center; + left: 50%; + padding-left: 0.15rem; + position: absolute; + top: 50%; + transform: translate(-50%, -50%); + width: 3.25rem; + z-index: 1; +} + +.oc-video-resource-list__editor-warning { + align-items: center; + color: var(--oc-video-resource-primary); + display: flex; + font-weight: 600; + height: 100%; + justify-content: center; + padding: 1.5rem; + text-align: center; +} + +.oc-video-resource-list__body { + display: flex; + flex: 1 1 auto; + flex-direction: column; + padding: 1.35rem; +} + +.oc-video-resource-list__category { + color: rgba(20, 40, 30, 0.68); + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.08em; + line-height: 1.2; + text-transform: uppercase; +} + +.oc-video-resource-list__title { + color: var(--oc-video-resource-primary); + font-size: 1.18rem; + font-weight: 700; + line-height: 1.28; +} + +.oc-video-resource-list__card-description { + color: inherit; + flex: 1 1 auto; +} + +.oc-video-resource-list__body > .oc-video-resource-list__tags { + margin-top: auto !important; + padding-top: 1.25rem; +} + +.oc-video-resource-list__tags > li, +.oc-video-resource-list__tags > li + li { + margin-top: 0 !important; +} + +.oc-video-resource-list__tag { + background: var(--oc-video-resource-soft); + border: 1px solid rgba(var(--oc-video-resource-primary-rgb), 0.12); + border-radius: 999px; + color: var(--oc-video-resource-primary); + display: inline-flex; + font-size: 0.78rem; + font-weight: 600; + line-height: 1.2; + padding: 0.35rem 0.65rem; +} + +.oc-video-resource-list .oc-video-resource-list__button, +.oc-video-resource-list .oc-video-resource-list__button:link, +.oc-video-resource-list .oc-video-resource-list__button:visited { + background-color: var(--oc-video-resource-primary); + border-color: var(--oc-video-resource-primary); + border-radius: 999px; + color: #ffffff; + font-weight: 600; + padding-left: 1rem; + padding-right: 1rem; + width: fit-content; + text-decoration: none; +} + +.oc-video-resource-list__body > .oc-video-resource-list__tags + .oc-video-resource-list__button { + margin-top: 1.5rem !important; +} + +.oc-video-resource-list__body > .oc-video-resource-list__card-description + .oc-video-resource-list__button, +.oc-video-resource-list__body > .oc-video-resource-list__title + .oc-video-resource-list__button { + margin-top: auto !important; +} + +.oc-video-resource-list--compact .oc-video-resource-list__intro { + margin-bottom: 1rem; +} + +.oc-video-resource-list--compact .oc-video-resource-list__nav { + margin-bottom: 1rem; +} + +.oc-video-resource-list--compact .oc-video-resource-list__category-heading-wrap { + margin-bottom: 0.75rem; + margin-top: 0 !important; +} + +.oc-video-resource-list--compact .oc-video-resource-list__grid + .oc-video-resource-list__category-anchor { + margin-top: 2.25rem; +} + +.oc-video-resource-list--compact .oc-video-resource-list__body { + padding: 1rem; +} + +.oc-video-resource-list--compact .oc-video-resource-list__title { + font-size: 1.08rem; +} + +.oc-video-resource-list--grid .oc-video-resource-list__grid { + margin-top: 0.5rem; +} + +.oc-video-resource-list .oc-video-resource-list__button:hover, +.oc-video-resource-list .oc-video-resource-list__button:focus, +.oc-video-resource-list .oc-video-resource-list__button:active, +.oc-video-resource-list .oc-video-resource-list__button:focus-visible { + background-color: var(--oc-video-resource-primary-hover); + border-color: var(--oc-video-resource-primary-hover); + color: #ffffff; + text-decoration: none; +} + +.oc-video-resource-list .oc-video-resource-list__button i { + color: inherit; +} + +@media (max-width: 575.98px) { + .oc-video-resource-list__intro { + margin-bottom: 1.25rem; + } + + .oc-video-resource-list__category-heading-wrap { + margin-top: 1.5rem; + } + + .oc-video-resource-list__grid + .oc-video-resource-list__category-anchor { + margin-top: 2.5rem; + } + + .oc-video-resource-list__back-link { + margin-left: 0; + } + + .oc-video-resource-list__body { + padding: 1.1rem; + } + + .oc-video-resource-list__title { + font-size: 1.08rem; + } + + .oc-video-resource-list__card-description { + flex: initial; + } + + .oc-video-resource-list__body > .oc-video-resource-list__tags { + margin-top: 1rem !important; + padding-top: 0; + } + + .oc-video-resource-list__play { + height: 2.75rem; + width: 2.75rem; + } +} + +@media (min-width: 992px) { + .oc-video-resource-list__category { + min-height: 0.95rem; + } + + .oc-video-resource-list__title { + min-height: calc(1.18rem * 1.28 * 2); + } +}