diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 100e8cf73..bae5fadc3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,7 @@ We really want to make space for all developers to feel comfortable and supporte - [Tips and Gotchas](https://github.com/Virtual-Coffee/virtualcoffee.io/blob/main/CONTRIBUTING.md#tips-and-gotchas) - [Reporting A Bug](https://github.com/Virtual-Coffee/virtualcoffee.io/blob/main/CONTRIBUTING.md#reporting-a-bug) - [Labelling conventions](https://github.com/Virtual-Coffee/virtualcoffee.io/blob/main/CONTRIBUTING.md#labelling-conventions) +- [Content Tagging System](https://github.com/Virtual-Coffee/virtualcoffee.io/blob/main/CONTRIBUTING.md#content-tagging-system) ## What Type of Contributions We're Looking For @@ -161,6 +162,166 @@ This repository has three basic types of labels: - `Status` - What part of the process is this issue in. e.g. Active, Needs Review, Resolved, etc. (will be filled in by maintainers and some contributors). Only one active per issue. - `Context` - Additional info that helps people parse issues. e.g. "good first issue", "for maintainer only". Multiple may be active on one issue. +## Content Tagging System + +Our resource filtering system uses a structured tagging approach to help users find relevant content quickly. All content tags are organized into logical categories to improve discoverability and user experience. + +### Tag Categories + +#### 🎯 Skill Level + +Tags that indicate the experience level required for the resource: + +- **`beginner`** - Content suitable for those new to programming or the specific topic +- **`intermediate`** - Content for developers with some experience who want to deepen their knowledge +- **`advanced`** - Content for experienced developers looking to master complex topics + +#### 🛠️ Technologies + +Tags for programming languages, frameworks, and tools: + +- **`javascript`** - JavaScript language, ES6+, Node.js, and related concepts +- **`typescript`** - TypeScript language, type systems, and TypeScript-specific patterns +- **`react`** - React library, hooks, components, and React ecosystem +- **`node`** - Node.js runtime, server-side JavaScript, and backend development +- **`css`** - CSS styling, layouts, animations, and modern CSS features +- **`html`** - HTML markup, semantic elements, and web standards +- **`git`** - Git version control, branching strategies, and collaboration workflows + +#### 📚 Topics + +Subject areas and domains: + +- **`career`** - Career development, job searching, professional growth, and industry insights +- **`open-source`** - Contributing to open source, maintaining projects, and community involvement +- **`testing`** - Testing strategies, frameworks, TDD, and quality assurance +- **`deployment`** - Deployment strategies, CI/CD, hosting, and DevOps practices +- **`interviewing`** - Technical interviews, coding challenges, and interview preparation + +#### 📖 Format + +Type of content or learning material: + +- **`tutorial`** - Step-by-step instructional content with hands-on examples +- **`reference`** - Quick lookup guides, documentation, and technical specifications +- **`guide`** - Comprehensive walkthroughs and best practices +- **`tips`** - Short, actionable advice and quick wins + +### Tagging Guidelines + +#### When to Add New Tags + +**Add a new tag when:** + +- A new technology, framework, or tool becomes widely adopted in the community +- A new topic area emerges that doesn't fit existing categories +- Multiple resources would benefit from a more specific tag than existing ones +- The tag would help users find content more effectively + +**Don't add a new tag when:** + +- Only one or two resources would use the tag +- An existing tag already covers the concept adequately +- The tag is too specific or niche for general use +- The tag duplicates functionality of existing tags + +#### How to Add New Tags + +1. **Propose the tag** - Create an issue or discussion explaining: + - What the new tag represents + - Which category it belongs to + - Why existing tags don't suffice + - Examples of content that would use this tag + +2. **Get community feedback** - Allow time for maintainers and community members to discuss the proposal + +3. **Update the system** - Once approved, the tag will be added to the `TAG_CATEGORIES` configuration in `src/util/tagCategories.ts` + +#### Tagging Best Practices + +**Do:** + +- Use existing tags when they accurately describe your content +- Apply multiple relevant tags from different categories +- Be consistent with tag naming (lowercase, hyphenated for multi-word tags) +- Consider your audience's skill level when choosing skill-level tags +- Think about how users might search for your content + +**Don't:** + +- Create overly specific tags that only apply to one piece of content +- Use tags that are too broad or vague +- Apply tags that don't accurately represent the content +- Use inconsistent naming conventions +- Over-tag content (3-5 relevant tags are usually sufficient) + +### Examples of Proper Tagging + +#### Example 1: React Tutorial + +```yaml +contentTags: + - beginner + - react + - javascript + - tutorial +``` + +**Rationale:** This is a beginner-friendly tutorial about React (JavaScript framework), so it gets skill level, technology, and format tags. + +#### Example 2: Career Guide + +```yaml +contentTags: + - intermediate + - career + - guide +``` + +**Rationale:** This is an intermediate-level career development guide, covering the topic and format categories. + +#### Example 3: Advanced Testing Resource + +```yaml +contentTags: + - advanced + - testing + - javascript + - reference +``` + +**Rationale:** This is an advanced reference for JavaScript testing, covering skill level, topic, technology, and format. + +#### Example 4: Open Source Contribution Tips + +```yaml +contentTags: + - intermediate + - open-source + - git + - tips +``` + +**Rationale:** This provides intermediate-level tips for contributing to open source projects using Git, covering multiple relevant categories. + +### Tag Maintenance + +- **Regular Review**: Tags are reviewed periodically to ensure they remain relevant and useful +- **Community Input**: Suggestions for new tags or modifications are welcome through issues or discussions +- **Documentation Updates**: This documentation is updated when new tags are added or existing ones are modified +- **Migration**: When tags are deprecated or renamed, existing content is updated accordingly + +### Questions About Tagging? + +If you're unsure about which tags to use for your content, or if you think a new tag might be needed, please: + +1. Check existing similar content to see what tags are commonly used +2. Ask in the [discussions board](https://github.com/Virtual-Coffee/virtualcoffee.io/discussions) +3. Create an issue with your tagging question +4. Reach out to maintainers for guidance + +Remember: Good tagging helps the entire community find and benefit from your contributions! + ## Add yourself to the Contributor's list We want to thank you so much for contributing to our project. Check out all [contributor's documentation](https://allcontributors.org/docs/en/bot/usage) to learn how to add yourself to our Contributor's list. diff --git a/TAGGING_GUIDE.md b/TAGGING_GUIDE.md new file mode 100644 index 000000000..cfdfd9f87 --- /dev/null +++ b/TAGGING_GUIDE.md @@ -0,0 +1,93 @@ +# Content Tagging Quick Reference + +## 🏷️ Available Tags by Category + +### 🎯 Skill Level + +- `beginner` - New to programming or topic +- `intermediate` - Some experience, deepening knowledge +- `advanced` - Experienced, mastering complex topics + +### 🛠️ Technologies + +- `javascript` - JavaScript, ES6+, Node.js +- `typescript` - TypeScript language and patterns +- `react` - React library and ecosystem +- `node` - Node.js runtime and backend +- `css` - CSS styling and layouts +- `html` - HTML markup and standards +- `git` - Version control and workflows + +### 📚 Topics + +- `career` - Career development and growth +- `open-source` - Contributing and maintaining projects +- `testing` - Testing strategies and frameworks +- `deployment` - CI/CD, hosting, DevOps +- `interviewing` - Technical interviews and prep + +### 📖 Format + +- `tutorial` - Step-by-step instructions +- `reference` - Quick lookup guides +- `guide` - Comprehensive walkthroughs +- `tips` - Short, actionable advice + +## ✅ Tagging Best Practices + +### Do: + +- Use 3-5 relevant tags maximum +- Apply tags from different categories +- Use lowercase, hyphenated naming +- Consider your audience's skill level +- Think about how users will search + +### Don't: + +- Create overly specific tags +- Use tags that don't match content +- Over-tag (more than 5 tags) +- Use inconsistent naming +- Apply vague or broad tags + +## 📝 Example Tagging + +```yaml +# React Tutorial for Beginners +contentTags: + - beginner + - react + - javascript + - tutorial + +# Career Development Guide +contentTags: + - intermediate + - career + - guide + +# Advanced Testing Reference +contentTags: + - advanced + - testing + - javascript + - reference +``` + +## 🔄 Adding New Tags + +1. **Propose** - Create issue/discussion explaining the need +2. **Discuss** - Get community feedback +3. **Implement** - Update `src/util/tagCategories.ts` once approved + +## ❓ Need Help? + +- Check existing similar content for tag patterns +- Ask in [discussions](https://github.com/Virtual-Coffee/virtualcoffee.io/discussions) +- Create an issue with your question +- Reach out to maintainers + +--- + +_For detailed guidelines, see [CONTRIBUTING.md#content-tagging-system](CONTRIBUTING.md#content-tagging-system)_ diff --git a/src/components/PostList.tsx b/src/components/PostList.tsx index 3cbe2b20d..5f9176c09 100644 --- a/src/components/PostList.tsx +++ b/src/components/PostList.tsx @@ -2,15 +2,17 @@ import { MdxFile } from '@/util/loadMdx.server'; import Link from 'next/link'; import path from 'path'; import { ReactNode } from 'react'; +import TagBadge from './TagBadge'; /* PostListItem is each resource under a section of content on the homepage. */ -type PostListItem = { +export type PostListItem = { href?: string; title: string; description?: string | ReactNode; children?: PostListItem[] | null; + contentTags?: string[]; }; type TitleProps = { @@ -41,7 +43,21 @@ export default function PostList({ items }: { items: PostListItem[] | null }) { {items.map((item, key) => { return (
  • - +
    + + {item.contentTags && item.contentTags.length > 0 && ( +
    + {item.contentTags.map((tag, tagIndex) => ( + + ))} +
    + )} +
    {item.description && (

    {item.description}

    )} @@ -68,16 +84,17 @@ export function formatFileListItemsForPostList( if (!items || internalCurLevel >= depth) { return null; } - return items.map((item): PostListItem => { const parts = item.slug.split(path.sep).filter((part) => { return !!part && part !== 'content'; }); + return { title: item.meta.title, description: item.meta.description, href: `/${parts.join('/')}`, + contentTags: item.contentTags || [], children: formatFileListItemsForPostList( item.children, depth, diff --git a/src/components/TagBadge.tsx b/src/components/TagBadge.tsx new file mode 100644 index 000000000..63fc05af1 --- /dev/null +++ b/src/components/TagBadge.tsx @@ -0,0 +1,62 @@ +import { useState } from 'react'; + +interface TagBadgeProps { + tag: string; + isSelected?: boolean; + onClick?: (tag: string, selected: boolean) => void; + variant?: 'default' | 'filter'; + size?: 'sm' | 'md'; + disabled?: boolean; +} + +export default function TagBadge({ + tag, + isSelected = false, + onClick, + variant = 'default', + size = 'md', + disabled = false, +}: TagBadgeProps) { + const handleClick = () => { + if (!disabled && onClick) { + onClick(tag, !isSelected); + } + }; + + const baseClasses = 'badge tag-badge'; + const variantClasses = { + default: 'badge-secondary', + filter: isSelected ? 'badge-primary' : 'badge-outline-secondary', + }; + const sizeClasses = { + sm: 'tag-badge-sm', + md: '', + }; + const interactiveClasses = onClick && !disabled ? 'tag-badge-interactive' : ''; + const disabledClasses = disabled ? 'tag-badge-disabled' : ''; + + const className = [ + baseClasses, + variantClasses[variant], + sizeClasses[size], + interactiveClasses, + disabledClasses, + ] + .filter(Boolean) + .join(' '); + + const TagComponent = onClick && !disabled ? 'button' : 'span'; + + return ( + + {tag} + + ); +} \ No newline at end of file diff --git a/src/components/TagFilteredResourceList.tsx b/src/components/TagFilteredResourceList.tsx new file mode 100644 index 000000000..1f3fe2261 --- /dev/null +++ b/src/components/TagFilteredResourceList.tsx @@ -0,0 +1,198 @@ +'use client'; + +import { useState, useMemo, useCallback } from 'react'; +import PostList, { PostListItem } from '@/components/PostList'; +import TagBadge from '@/components/TagBadge'; +import { organizeTagsByCategories, TagGrouping } from '@/util/tagCategories'; + +interface TagFilteredResourceListProps { + allTags: string[]; + processedPostListItems: PostListItem[] | null; +} + +export default function TagFilteredResourceList({ + allTags, + processedPostListItems, +}: TagFilteredResourceListProps) { + const [selectedTags, setSelectedTags] = useState>(new Set()); + + const tagGrouping = useMemo(() => { + return organizeTagsByCategories(allTags); + }, [allTags]); + + const filteredPostListItems = useMemo(() => { + if (selectedTags.size === 0 || !processedPostListItems) { + return processedPostListItems; + } + + const filterPostListItems = (items: PostListItem[]): PostListItem[] => { + const requiredTags = Array.from(selectedTags); + return items + .map((item): PostListItem | null => { + const itemTags = item.contentTags ?? []; + const itemMatches = requiredTags.every((tag) => itemTags.includes(tag)); + + // Filter children recursively + const filteredChildren = item.children + ? filterPostListItems(item.children) + : undefined; + + // Include item if it matches or has matching children + if (itemMatches || (filteredChildren && filteredChildren.length > 0)) { + return { + ...item, + children: filteredChildren, + }; + } + + return null; + }) + .filter((item): item is PostListItem => item !== null); + }; + + return filterPostListItems(processedPostListItems); + }, [selectedTags, processedPostListItems]); + + const handleTagToggle = useCallback((tag: string, selected: boolean) => { + setSelectedTags((prev) => { + const newSet = new Set(prev); + if (selected) { + newSet.add(tag); + } else { + newSet.delete(tag); + } + return newSet; + }); + }, []); + + const handleClearAll = useCallback(() => { + setSelectedTags(new Set()); + }, []); + + const resourceCount = useMemo(() => { + const countRecursive = (items: PostListItem[]): number => { + return items.reduce((count, item) => { + return count + 1 + (item.children ? countRecursive(item.children) : 0); + }, 0); + }; + return filteredPostListItems ? countRecursive(filteredPostListItems) : 0; + }, [filteredPostListItems]); + + const totalResourceCount = useMemo(() => { + const countRecursive = (items: PostListItem[]): number => { + return items.reduce((count, item) => { + return count + 1 + (item.children ? countRecursive(item.children) : 0); + }, 0); + }; + return processedPostListItems ? countRecursive(processedPostListItems) : 0; + }, [processedPostListItems]); + + + return ( +
    + {allTags.length > 0 && ( +
    +
    +

    Filter by Tags

    + {selectedTags.size > 0 && ( + + )} +
    + +
    + {/* Render categorized tags */} + {tagGrouping.categories.map((category) => ( +
    +
    +
    + {category.name} +
    + {category.description && ( + + {category.description} + + )} +
    +
    + {category.tags.map((tag) => ( + + ))} +
    +
    + ))} + + {/* Render uncategorized tags if any */} + {tagGrouping.uncategorizedTags.length > 0 && ( +
    +
    +
    Other
    + + Additional tags + +
    +
    + {tagGrouping.uncategorizedTags.map((tag) => ( + + ))} +
    +
    + )} +
    + + {selectedTags.size > 0 && ( +
    + + Showing {resourceCount} of {totalResourceCount} resources + {selectedTags.size > 0 && ( + <> + {' '} + matching:{' '} + {Array.from(selectedTags) + .map((tag) => `"${tag}"`) + .join(', ')} + + )} + +
    + )} +
    + )} + + {filteredPostListItems && filteredPostListItems.length > 0 ? ( + + ) : selectedTags.size > 0 ? ( +
    +

    + No resources found matching the selected tags. Try selecting different tags or{' '} + + . +

    +
    + ) : null} +
    + ); +} diff --git a/src/components/content/FileIndex.tsx b/src/components/content/FileIndex.tsx index 1e8e0454e..f0cb39434 100644 --- a/src/components/content/FileIndex.tsx +++ b/src/components/content/FileIndex.tsx @@ -1,20 +1,28 @@ -import PostList, { - formatFileListItemsForPostList, -} from '@/components/PostList'; - import { loadMdxDirectory } from '@/util/loadMdx.server'; +import { formatFileListItemsForPostList } from '@/components/PostList'; +import { extractAllContentTags } from '@/util/extractContentTags'; +import TagFilteredResourceListWrapper from './TagFilteredResourceListWrapper'; type FileIndexProps = { subDirectory?: string; depth?: number; }; -export default function FileIndex({ subDirectory, depth }: FileIndexProps) { +export default function FileIndex({ + subDirectory, + depth, +}: FileIndexProps) { const allFiles = loadMdxDirectory({ baseDirectory: 'content' + (subDirectory ? `/${subDirectory}` : ''), }); - // const result = subDirectory ? findBase(allFiles, subDirectory) : allFiles; - const result = allFiles; - return ; + const processedPostListItems = formatFileListItemsForPostList(allFiles, depth); + const allTags = extractAllContentTags(allFiles); + + return ( + + ); } diff --git a/src/components/content/TagFilteredResourceListWrapper.tsx b/src/components/content/TagFilteredResourceListWrapper.tsx new file mode 100644 index 000000000..7e2d60ba4 --- /dev/null +++ b/src/components/content/TagFilteredResourceListWrapper.tsx @@ -0,0 +1,26 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import { PostListItem } from '../PostList'; + +const TagFilteredResourceList = dynamic( + () => import('@/components/TagFilteredResourceList'), + { ssr: false } +); + +interface TagFilteredResourceListWrapperProps { + allTags: string[]; + processedPostListItems: PostListItem[] | null; +} + +export default function TagFilteredResourceListWrapper({ + allTags, + processedPostListItems, +}: TagFilteredResourceListWrapperProps) { + return ( + + ); +} diff --git a/src/content/resources/developer-resources/developer-health/burnout.mdx b/src/content/resources/developer-resources/developer-health/burnout.mdx index 5fb8be595..42d8aae49 100644 --- a/src/content/resources/developer-resources/developer-health/burnout.mdx +++ b/src/content/resources/developer-resources/developer-health/burnout.mdx @@ -7,6 +7,10 @@ hero: tags: - memberresources - memberresourcesIndex +contentTags: + - beginner + - career + - guide order: 1 --- diff --git a/src/content/resources/developer-resources/developer-health/neurodiverse.mdx b/src/content/resources/developer-resources/developer-health/neurodiverse.mdx index 18886df0e..e6dc2b2b6 100644 --- a/src/content/resources/developer-resources/developer-health/neurodiverse.mdx +++ b/src/content/resources/developer-resources/developer-health/neurodiverse.mdx @@ -7,6 +7,10 @@ hero: tags: - memberresources - memberresourcesIndex +contentTags: + - beginner + - career + - guide order: 2 --- diff --git a/src/content/resources/developer-resources/developer-tips/asking-coding-questions.mdx b/src/content/resources/developer-resources/developer-tips/asking-coding-questions.mdx index 7b5b83843..7e4ec84a2 100644 --- a/src/content/resources/developer-resources/developer-tips/asking-coding-questions.mdx +++ b/src/content/resources/developer-resources/developer-tips/asking-coding-questions.mdx @@ -4,6 +4,10 @@ meta: description: A guide for developers to ask questions about code. hero: Hero: UndrawQuestions +contentTags: + - beginner + - guide + - tips order: 1 --- diff --git a/src/content/resources/developer-resources/job-hunt/job-hunt-with-AI.mdx b/src/content/resources/developer-resources/job-hunt/job-hunt-with-AI.mdx index 834bddb25..aa3115b61 100644 --- a/src/content/resources/developer-resources/job-hunt/job-hunt-with-AI.mdx +++ b/src/content/resources/developer-resources/job-hunt/job-hunt-with-AI.mdx @@ -5,6 +5,12 @@ meta: hero: Hero: UndrawChatWithAI tags: [memberresources] +contentTags: + - intermediate + - career + - ai + - interviewing + - guide order: 1 --- diff --git a/src/content/resources/developer-resources/open-source/about-open-source.mdx b/src/content/resources/developer-resources/open-source/about-open-source.mdx index 98c1ae2ec..f91357326 100644 --- a/src/content/resources/developer-resources/open-source/about-open-source.mdx +++ b/src/content/resources/developer-resources/open-source/about-open-source.mdx @@ -3,6 +3,10 @@ meta: title: About Open Source description: A general guide to all things open source. tags: [memberresources] +contentTags: + - beginner + - open-source + - guide hero: Hero: UndrawOnlinePage order: 1 diff --git a/src/content/resources/developer-resources/open-source/contributor-guide.mdx b/src/content/resources/developer-resources/open-source/contributor-guide.mdx index 59b179283..570e05986 100644 --- a/src/content/resources/developer-resources/open-source/contributor-guide.mdx +++ b/src/content/resources/developer-resources/open-source/contributor-guide.mdx @@ -3,6 +3,10 @@ meta: title: Contributor Guide description: A guide to contributing to open source. tags: [memberresources] +contentTags: + - intermediate + - open-source + - guide hero: Hero: UndrawDeveloperActivity order: 3 diff --git a/src/content/resources/developer-resources/open-source/git-101.mdx b/src/content/resources/developer-resources/open-source/git-101.mdx index 59610861f..b1e61bffa 100644 --- a/src/content/resources/developer-resources/open-source/git-101.mdx +++ b/src/content/resources/developer-resources/open-source/git-101.mdx @@ -3,6 +3,11 @@ meta: title: Git & GitHub 101 description: A guide to using Git and GitHub. tags: [memberresources] +contentTags: + - beginner + - git + - open-source + - tutorial hero: Hero: UndrawSoftwareEngineer order: 2 diff --git a/src/content/resources/developer-resources/open-source/maintainer-guide.mdx b/src/content/resources/developer-resources/open-source/maintainer-guide.mdx index 4c9534c35..0aa2a4a6a 100644 --- a/src/content/resources/developer-resources/open-source/maintainer-guide.mdx +++ b/src/content/resources/developer-resources/open-source/maintainer-guide.mdx @@ -3,6 +3,10 @@ meta: title: Maintainer Guide description: A guide to maintaining open source projects. tags: [memberresources] +contentTags: + - advanced + - open-source + - guide hero: Hero: UndrawOperatingSystem order: 4 diff --git a/src/content/resources/virtual-coffee-handbook/get-involved/leading-coffee-table-groups.mdx b/src/content/resources/virtual-coffee-handbook/get-involved/leading-coffee-table-groups.mdx index 6db308fcc..3680594da 100644 --- a/src/content/resources/virtual-coffee-handbook/get-involved/leading-coffee-table-groups.mdx +++ b/src/content/resources/virtual-coffee-handbook/get-involved/leading-coffee-table-groups.mdx @@ -5,6 +5,10 @@ meta: hero: Hero: UndrawConversation tags: [memberresources] +contentTags: + - intermediate + - career + - guide order: 2 --- diff --git a/src/content/resources/virtual-coffee-handbook/get-involved/paths-to-leadership.mdx b/src/content/resources/virtual-coffee-handbook/get-involved/paths-to-leadership.mdx index 485a06346..e42c21aa8 100644 --- a/src/content/resources/virtual-coffee-handbook/get-involved/paths-to-leadership.mdx +++ b/src/content/resources/virtual-coffee-handbook/get-involved/paths-to-leadership.mdx @@ -5,6 +5,10 @@ meta: hero: Hero: UndrawPowerful tags: [memberresources] +contentTags: + - intermediate + - career + - guide order: 1 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/code-of-conduct.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/code-of-conduct.mdx index 23840fe9c..31aae3c05 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/code-of-conduct.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/code-of-conduct.mdx @@ -5,6 +5,9 @@ meta: hero: Hero: UndrawAgreement tags: [memberresources] +contentTags: + - beginner + - reference order: 1 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/coffee-table-groups.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/coffee-table-groups.mdx index c6132fc27..729e2beee 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/coffee-table-groups.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/coffee-table-groups.mdx @@ -5,6 +5,9 @@ meta: hero: Hero: UndrawConversation tags: [memberresources] +contentTags: + - beginner + - reference order: 4 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/giving-back-to-the-community.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/giving-back-to-the-community.mdx index b2a0fddd6..87779325a 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/giving-back-to-the-community.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/giving-back-to-the-community.mdx @@ -5,6 +5,9 @@ meta: hero: Hero: UndrawTeamCollaboration tags: [memberresources] +contentTags: + - beginner + - guide order: 9 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/glossary.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/glossary.mdx index 8a5ab698a..2ebebd3d0 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/glossary.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/glossary.mdx @@ -5,6 +5,9 @@ meta: hero: Hero: UndrawReadingList tags: [memberresources] +contentTags: + - beginner + - reference order: 11 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/hacktoberfest-initiative.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/hacktoberfest-initiative.mdx index f46ee5587..c3854e8b1 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/hacktoberfest-initiative.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/hacktoberfest-initiative.mdx @@ -5,6 +5,10 @@ meta: hero: Hero: UndrawPumpkin tags: [memberresources] +contentTags: + - beginner + - open-source + - guide order: 8 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/keeping-up-with-virtual-coffee.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/keeping-up-with-virtual-coffee.mdx index 85bcb8968..b67f3aa21 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/keeping-up-with-virtual-coffee.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/keeping-up-with-virtual-coffee.mdx @@ -5,6 +5,9 @@ meta: hero: Hero: UndrawSocialUser tags: [memberresources] +contentTags: + - beginner + - guide order: 10 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/lightning-talk.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/lightning-talk.mdx index 8eab20c4c..e16541e2c 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/lightning-talk.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/lightning-talk.mdx @@ -5,6 +5,10 @@ meta: hero: Hero: UndrawConference tags: [memberresources] +contentTags: + - beginner + - career + - guide order: 7 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/lunch-and-learns.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/lunch-and-learns.mdx index c31088490..5414cc6b4 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/lunch-and-learns.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/lunch-and-learns.mdx @@ -5,6 +5,10 @@ meta: hero: Hero: UndrawPresentation tags: [memberresources] +contentTags: + - beginner + - career + - guide order: 6 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/monthly-challenges.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/monthly-challenges.mdx index f2cf88550..f672e3880 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/monthly-challenges.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/monthly-challenges.mdx @@ -5,6 +5,9 @@ meta: hero: Hero: UndrawGoodTeam tags: [memberresources] +contentTags: + - beginner + - guide order: 5 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/slack-channels-guide.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/slack-channels-guide.mdx index 5327b5b93..88b95bf72 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/slack-channels-guide.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/slack-channels-guide.mdx @@ -5,6 +5,9 @@ meta: hero: Hero: UndrawQuickChat tags: [memberresources] +contentTags: + - beginner + - reference order: 3 --- diff --git a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/what-to-expect-in-virtual-coffee.mdx b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/what-to-expect-in-virtual-coffee.mdx index 4240ec41b..e9e33b897 100644 --- a/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/what-to-expect-in-virtual-coffee.mdx +++ b/src/content/resources/virtual-coffee-handbook/guides-to-virtual-coffee/what-to-expect-in-virtual-coffee.mdx @@ -5,6 +5,9 @@ meta: hero: Hero: UndrawRemoteMeeting tags: [memberresources] +contentTags: + - beginner + - guide order: 2 --- diff --git a/src/content/resources/virtual-coffee-handbook/join-virtual-coffee/index.mdx b/src/content/resources/virtual-coffee-handbook/join-virtual-coffee/index.mdx index 23a3b4267..fad9900da 100644 --- a/src/content/resources/virtual-coffee-handbook/join-virtual-coffee/index.mdx +++ b/src/content/resources/virtual-coffee-handbook/join-virtual-coffee/index.mdx @@ -6,6 +6,9 @@ hero: Hero: UndrawJoin tags: - memberresources +contentTags: + - beginner + - guide order: 1 --- diff --git a/src/styles/_postlist.scss b/src/styles/_postlist.scss index 3bb604ad1..18adfca16 100644 --- a/src/styles/_postlist.scss +++ b/src/styles/_postlist.scss @@ -9,6 +9,17 @@ ul.postlist { .postlist-title { font-size: 1.2em; } + + .postlist-header { + @include media-breakpoint-down(md) { + flex-direction: column; + align-items: flex-start; + } + } + + .postlist-tags { + gap: 0.375rem; + } } } @@ -20,6 +31,32 @@ ul.postlist { font-size: 1.5em; } +.postlist-header { + display: flex; + flex-direction: column; + gap: 0.75rem; + + @include media-breakpoint-up(md) { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 1rem; + } +} + +.postlist-tags { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + margin-top: 0.25rem; + + @include media-breakpoint-up(md) { + margin-top: 0; + flex-shrink: 0; + } +} + .homepageblocks { .postlist-item { margin-bottom: 1em; @@ -30,4 +67,21 @@ ul.postlist { font-size: 1em; font-weight: 500; } + + .postlist-header { + gap: 0.5rem; + + @include media-breakpoint-up(md) { + gap: 0.75rem; + } + } + + .postlist-tags { + gap: 0.375rem; + margin-top: 0.375rem; + + @include media-breakpoint-up(md) { + margin-top: 0; + } + } } diff --git a/src/styles/_tag-filter.scss b/src/styles/_tag-filter.scss new file mode 100644 index 000000000..abb907899 --- /dev/null +++ b/src/styles/_tag-filter.scss @@ -0,0 +1,242 @@ +.tag-badge { + display: inline-flex; + align-items: center; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.2; + padding: 0.375rem 0.75rem; + border-radius: $border-radius; + border: 1px solid transparent; + text-decoration: none; + transition: all 0.15s ease-in-out; + cursor: default; + user-select: none; + + &.tag-badge-sm { + font-size: 0.75rem; + padding: 0.25rem 0.5rem; + } + + &.tag-badge-interactive { + cursor: pointer; + + &:hover:not(:disabled) { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + &:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + } + + &:focus { + outline: none; + box-shadow: 0 0 0 2px rgba($blue, 0.25); + } + } + + &.tag-badge-disabled { + opacity: 0.6; + cursor: not-allowed; + } + + &.badge-primary { + background-color: $pink; + color: $white; + border-color: $pink; + + &:hover:not(:disabled) { + background-color: darken($pink, 8%); + border-color: darken($pink, 8%); + } + } + + &.badge-outline-secondary { + color: $blackCoral; + border-color: rgba($blackCoral, 0.3); + background-color: transparent; + + &:hover:not(:disabled) { + background-color: rgba($blackCoral, 0.05); + border-color: rgba($blackCoral, 0.5); + } + } + + &.badge-secondary { + background-color: rgba($blackCoral, 0.1); + color: $blackCoral; + border-color: rgba($blackCoral, 0.1); + } +} + +.tag-filtered-resource-list { + margin-bottom: map-get($spacers, 4); + + .resource-tag-filter-section { + margin-bottom: map-get($spacers, 3); + padding-left: map-get($spacers, 3); + padding-right: map-get($spacers, 3); + padding-bottom: map-get($spacers, 1); + background-color: rgba($gray-200, 0.3); + border-radius: $border-radius; + border: 1px solid rgba($gray-200, 0.6); + } + + .resource-tag-filter-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: map-get($spacers, 3); + + @include media-breakpoint-down(sm) { + flex-direction: column; + align-items: flex-start; + gap: map-get($spacers, 2); + } + } + + .resource-tag-filter-title { + margin: 0; + font-size: 1.25rem; + color: $gray-800; + font-weight: 600; + } + + .resource-tag-filter-clear-container { + display: flex; + align-items: center; + min-height: 2rem; + } + + .resource-tag-filter-clear { + color: $pink; + font-size: 0.875rem; + padding: 0.25rem 0.5rem; + text-decoration: none; + + &:hover { + color: darken($pink, 10%); + text-decoration: underline; + } + } + + .resource-tag-filter-clear-placeholder { + display: inline-block; + height: 1.5rem; + width: 0; + } + + .resource-tag-filter-tags { + display: flex; + flex-direction: column; + gap: map-get($spacers, 3); + margin-bottom: map-get($spacers, 3); + } + + .tag-category-group { + display: flex; + flex-direction: column; + gap: map-get($spacers, 2); + } + + .tag-category-header { + display: flex; + flex-direction: column; + gap: 0.25rem; + } + + .tag-category-title { + margin: 0; + font-size: 1rem; + font-weight: 600; + line-height: 1.2; + } + + .tag-category-description { + font-size: 0.75rem; + color: $gray-600; + font-style: italic; + line-height: 1.2; + } + + .tag-category-tags { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + padding-left: map-get($spacers, 2); + border-left: 2px solid rgba($gray-300, 0.5); + margin-left: map-get($spacers, 1); + + @include media-breakpoint-down(sm) { + padding-left: map-get($spacers, 1); + margin-left: 0; + border-left: none; + border-top: 1px solid rgba($gray-300, 0.3); + padding-top: map-get($spacers, 1); + } + } + + .resource-tag-filter-status { + padding-top: map-get($spacers, 2); + border-top: 1px solid rgba($gray-200, 0.8); + font-size: 0.875rem; + min-height: 1.5rem; + display: flex; + align-items: center; + } + + .resource-tag-filter-status-placeholder { + display: inline-block; + height: 1.25rem; + width: 0; + } + + .resource-tag-filter-empty { + text-align: center; + padding: map-get($spacers, 4); + + p { + margin: 0; + font-size: 1rem; + } + + .btn-inline { + padding: 0; + font-size: inherit; + vertical-align: baseline; + color: $pink; + + &:hover { + color: darken($pink, 10%); + text-decoration: underline; + } + } + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.resource-tag-filter-section { + animation: fadeIn 0.3s ease-out; +} + +.tag-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + + &.tag-list-inline { + display: inline-flex; + } +} diff --git a/src/styles/main.scss b/src/styles/main.scss index 28c2a6a23..53d5a9786 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -12,3 +12,4 @@ @import './homepageblocks'; @import './supporters'; @import './gridlist'; +@import './tag-filter'; diff --git a/src/util/extractContentTags.ts b/src/util/extractContentTags.ts new file mode 100644 index 000000000..3ad33a962 --- /dev/null +++ b/src/util/extractContentTags.ts @@ -0,0 +1,28 @@ +import { MdxFile } from './loadMdx.server'; + +/** + * Recursively extracts all contentTags from an array of MdxFile objects and their children. + * @param mdxFiles - Array of MdxFile objects to extract contentTags from + * @returns Array of unique contentTags found in all files and their children + */ +export function extractAllContentTags(mdxFiles: MdxFile[]): string[] { + const allContentTags = new Set(); + + /** + * Recursive helper function to process a single MdxFile and its children + * @param mdxFile - The MdxFile to process + */ + function processMdxFile(mdxFile: MdxFile): void { + if (mdxFile.contentTags && Array.isArray(mdxFile.contentTags)) { + mdxFile.contentTags.forEach((tag) => allContentTags.add(tag)); + } + + if (mdxFile.children && Array.isArray(mdxFile.children)) { + mdxFile.children.forEach((child) => processMdxFile(child)); + } + } + + mdxFiles.forEach((mdxFile) => processMdxFile(mdxFile)); + + return Array.from(allContentTags).sort(); +} diff --git a/src/util/loadMdx.server.ts b/src/util/loadMdx.server.ts index 625893025..af2ff10c8 100644 --- a/src/util/loadMdx.server.ts +++ b/src/util/loadMdx.server.ts @@ -20,6 +20,7 @@ export interface MdxFile { heroSubheader?: string; }; tags?: string[]; + contentTags?: string[]; children?: MdxFile[]; } @@ -79,11 +80,27 @@ export function loadMdxDirectory({ }); if (dirIndex) { + const subChildren = loadMdxDirectory({ + baseDirectory: join(baseDirectory, dir.name, e.name), + }); + + if (subChildren && subChildren.length > 0) { + const childContentTags = subChildren + .filter( + (child) => + child.contentTags && child.contentTags.length > 0, + ) + .map((child) => child.contentTags!) + .flat(); + + dirIndex.contentTags = Array.from( + new Set(childContentTags), + ); + } + return { ...dirIndex, - children: loadMdxDirectory({ - baseDirectory: join(baseDirectory, dir.name, e.name), - }), + children: subChildren, }; } } @@ -97,6 +114,14 @@ export function loadMdxDirectory({ : 0; }); } + if (index && children) { + const childContentTags = children + .filter((child) => child.contentTags && child.contentTags.length > 0) + .map((child) => child.contentTags!) + .flat(); + + index.contentTags = Array.from(new Set(childContentTags)); + } return { ...index, @@ -182,6 +207,7 @@ export function loadMdxRouteFileAttributes({ // Parse the front matter from the file contents using the front-matter library const contents = fm(fileContents); + const attributes = contents.attributes as Omit< MdxFile, 'slug' | 'requirePath' diff --git a/src/util/tagCategories.ts b/src/util/tagCategories.ts new file mode 100644 index 000000000..44f6780c5 --- /dev/null +++ b/src/util/tagCategories.ts @@ -0,0 +1,96 @@ +/** + * Color palette for tag categories + */ +export const TAG_CATEGORY_COLORS = { + RED: '#e74c3c', + BLUE: '#3498db', + GREEN: '#2ecc71', + ORANGE: '#f39c12', +} as const; + +export interface TagCategory { + id: string; + name: string; + description?: string; + tags: string[]; + color?: string; +} + +export interface TagGrouping { + categories: TagCategory[]; + uncategorizedTags: string[]; +} + +/** + * Predefined tag categories as specified + */ +export const TAG_CATEGORIES: TagCategory[] = [ + { + id: 'skill-level', + name: 'Skill Level', + description: 'Experience level required for the resource', + tags: ['beginner', 'intermediate', 'advanced'], + color: TAG_CATEGORY_COLORS.RED, + }, + { + id: 'technologies', + name: 'Technologies', + description: 'Programming languages, frameworks, and tools', + tags: [ + 'javascript', + 'typescript', + 'react', + 'node', + 'css', + 'html', + 'git', + 'ai', + ], + color: TAG_CATEGORY_COLORS.BLUE, + }, + { + id: 'topics', + name: 'Topics', + description: 'Subject areas and domains', + tags: ['career', 'open-source', 'testing', 'deployment', 'interviewing'], + color: TAG_CATEGORY_COLORS.GREEN, + }, + { + id: 'format', + name: 'Format', + description: 'Type of content or learning material', + tags: ['tutorial', 'reference', 'guide', 'tips'], + color: TAG_CATEGORY_COLORS.ORANGE, + }, +]; + +/** + * Organizes tags into categories and identifies uncategorized tags + * @param allTags - Array of all available tags + * @returns TagGrouping with categorized and uncategorized tags + */ +export function organizeTagsByCategories(allTags: string[]): TagGrouping { + const categorizedTags = new Set(); + const categories: TagCategory[] = []; + + TAG_CATEGORIES.forEach((category) => { + const availableTags = category.tags.filter((tag) => allTags.includes(tag)); + + if (availableTags.length > 0) { + categories.push({ + ...category, + tags: availableTags, + }); + availableTags.forEach((tag) => categorizedTags.add(tag)); + } + }); + + const uncategorizedTags = allTags + .filter((tag) => !categorizedTags.has(tag)) + .sort(); + + return { + categories, + uncategorizedTags, + }; +}