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 && (
+
+ Clear all ({selectedTags.size})
+
+ )}
+
+
+
+ {/* 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{' '}
+
+ clear all filters
+
+ .
+
+
+ ) : 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,
+ };
+}