From 415b22f0954107516ba21c5a889577c91793ffa4 Mon Sep 17 00:00:00 2001 From: rebelchris Date: Tue, 10 Feb 2026 04:51:12 +0000 Subject: [PATCH 01/13] feat(skill-hub): add skill hub mock page --- .../skillHub/components/SkillCard.tsx | 146 +++++++ .../skillHub/components/SkillGrid.tsx | 35 ++ .../skillHub/components/SkillHubHeader.tsx | 81 ++++ .../skillHub/components/SkillRankingList.tsx | 66 ++++ .../shared/src/features/skillHub/mocks.ts | 364 ++++++++++++++++++ .../shared/src/features/skillHub/types.ts | 26 ++ packages/webapp/pages/skills/index.tsx | 61 +++ 7 files changed, 779 insertions(+) create mode 100644 packages/shared/src/features/skillHub/components/SkillCard.tsx create mode 100644 packages/shared/src/features/skillHub/components/SkillGrid.tsx create mode 100644 packages/shared/src/features/skillHub/components/SkillHubHeader.tsx create mode 100644 packages/shared/src/features/skillHub/components/SkillRankingList.tsx create mode 100644 packages/shared/src/features/skillHub/mocks.ts create mode 100644 packages/shared/src/features/skillHub/types.ts create mode 100644 packages/webapp/pages/skills/index.tsx diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx new file mode 100644 index 0000000000..63c9c3ec14 --- /dev/null +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -0,0 +1,146 @@ +import type { ReactElement, MouseEvent } from 'react'; +import React, { forwardRef } from 'react'; +import classNames from 'classnames'; +import type { Skill } from '../types'; +import { LazyImage } from '../../../components/LazyImage'; +import { + Typography, + TypographyTag, + TypographyType, +} from '../../../components/typography/Typography'; +import Link from '../../../components/utilities/Link'; +import { UpvoteIcon, DownloadIcon, DiscussIcon } from '../../../components/icons'; +import { IconSize } from '../../../components/Icon'; +import { largeNumberFormat } from '../../../lib/numberFormat'; +import { fallbackImages } from '../../../lib/config'; + +const NEW_SKILL_DAYS = 30; +const DAY_IN_MS = 24 * 60 * 60 * 1000; + +const isNewSkill = (createdAt: string): boolean => { + const createdAtMs = Date.parse(createdAt); + if (Number.isNaN(createdAtMs)) { + return false; + } + + return Date.now() - createdAtMs <= NEW_SKILL_DAYS * DAY_IN_MS; +}; + +interface SkillCardProps { + skill: Skill; + className?: string; +} + +export const SkillCard = forwardRef( + function SkillCardComponent({ skill, className }: SkillCardProps, ref): ReactElement { + const handleClick = (event: MouseEvent): void => { + event.preventDefault(); + }; + + const formattedUpvotes = largeNumberFormat(skill.upvotes) || '0'; + const formattedComments = largeNumberFormat(skill.comments) || '0'; + const formattedInstalls = largeNumberFormat(skill.installs) || '0'; + const newBadge = isNewSkill(skill.createdAt); + + return ( + + +
+ + {skill.category} + + {newBadge && ( + + New + + )} +
+
+ + {skill.displayName} + + + {skill.description} + +
+ {skill.tags.slice(0, 3).map((tag) => ( + + {tag} + + ))} +
+
+
+
+ +
+ + {skill.author.name} + + + {skill.author.isAgent ? 'Agent' : 'Human'} + +
+
+
+
+ + + {formattedUpvotes} + +
+
+ + + {formattedComments} + +
+
+ + + {formattedInstalls} + +
+
+
+
+ + ); + }, +); diff --git a/packages/shared/src/features/skillHub/components/SkillGrid.tsx b/packages/shared/src/features/skillHub/components/SkillGrid.tsx new file mode 100644 index 0000000000..e63dd54450 --- /dev/null +++ b/packages/shared/src/features/skillHub/components/SkillGrid.tsx @@ -0,0 +1,35 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import classNames from 'classnames'; +import type { Skill } from '../types'; +import { SkillCard } from './SkillCard'; +import { + Typography, + TypographyTag, + TypographyType, +} from '../../../components/typography/Typography'; + +interface SkillGridProps { + title: string; + skills: Skill[]; + className?: string; +} + +export const SkillGrid = ({ + title, + skills, + className, +}: SkillGridProps): ReactElement => { + return ( +
+ + {title} + +
+ {skills.map((skill) => ( + + ))} +
+
+ ); +}; diff --git a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx new file mode 100644 index 0000000000..30c9b7bc30 --- /dev/null +++ b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx @@ -0,0 +1,81 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import classNames from 'classnames'; +import { SearchField } from '../../../components/fields/SearchField'; +import { Button, ButtonSize, ButtonVariant } from '../../../components/buttons/Button'; +import { PlusIcon, SparkleIcon } from '../../../components/icons'; +import { + Typography, + TypographyTag, + TypographyType, +} from '../../../components/typography/Typography'; +import { IconSize } from '../../../components/Icon'; + +const categories = [ + 'All', + 'Code Review', + 'Database', + 'DevOps', + 'Testing', + 'Documentation', + 'Security', + 'Design', +]; + +export const SkillHubHeader = (): ReactElement => { + return ( +
+
+
+
+ + + +
+ + Skill Hub + + + Discover, share, and discuss skills for humans and agents. + +
+
+
+ {categories.map((category, index) => ( + + ))} +
+
+
+ + +
+
+
+ ); +}; diff --git a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx new file mode 100644 index 0000000000..567bd9290d --- /dev/null +++ b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx @@ -0,0 +1,66 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { Skill } from '../types'; +import { LeaderboardList } from '../../../components/cards/Leaderboard/LeaderboardList'; +import { LeaderboardListItem } from '../../../components/cards/Leaderboard/LeaderboardListItem'; +import { + Typography, + TypographyTag, + TypographyType, +} from '../../../components/typography/Typography'; +import { UpvoteIcon } from '../../../components/icons'; +import { IconSize } from '../../../components/Icon'; +import { largeNumberFormat } from '../../../lib/numberFormat'; + +interface SkillRankingListProps { + title: string; + skills: Skill[]; + className?: string; +} + +export const SkillRankingList = ({ + title, + skills, + className, +}: SkillRankingListProps): ReactElement => { + return ( + + {skills.map((skill, index) => ( + +
+
+ + {skill.displayName} + + + {skill.author.name} + +
+
+ + + {largeNumberFormat(skill.upvotes) || '0'} + +
+
+
+ ))} +
+ ); +}; diff --git a/packages/shared/src/features/skillHub/mocks.ts b/packages/shared/src/features/skillHub/mocks.ts new file mode 100644 index 0000000000..ab48fe9c01 --- /dev/null +++ b/packages/shared/src/features/skillHub/mocks.ts @@ -0,0 +1,364 @@ +import type { Skill } from './types'; + +export const skillHubMockData: Skill[] = [ + { + id: 'skill-001', + name: 'keyword-review', + displayName: 'Keyword Review', + description: + 'Highlights risky phrasing, missing scope, and ambiguous requirements before you ship.', + author: { + name: 'Riya Patel', + image: 'https://i.pravatar.cc/150?img=12', + isAgent: false, + }, + upvotes: 4820, + comments: 312, + installs: 32140, + category: 'Code Review', + tags: ['quality', 'linting', 'review'], + createdAt: '2025-11-04T09:12:00Z', + updatedAt: '2026-01-20T16:30:00Z', + trending: true, + }, + { + id: 'skill-002', + name: 'schema-guardian', + displayName: 'Schema Guardian', + description: + 'Generates Prisma migrations with guardrails and explains breaking changes.', + author: { + name: 'SchemaFox', + image: 'https://i.pravatar.cc/150?img=32', + isAgent: true, + }, + upvotes: 3760, + comments: 204, + installs: 19870, + category: 'Database', + tags: ['prisma', 'migrations', 'schema'], + createdAt: '2025-10-17T14:20:00Z', + updatedAt: '2026-01-28T12:45:00Z', + trending: true, + }, + { + id: 'skill-003', + name: 'deployment-diff', + displayName: 'Deployment Diff', + description: + 'Summarizes infra diffs and flags risky Terraform changes before apply.', + author: { + name: 'Ava Smith', + image: 'https://i.pravatar.cc/150?img=5', + isAgent: false, + }, + upvotes: 2995, + comments: 128, + installs: 14210, + category: 'DevOps', + tags: ['terraform', 'infra', 'review'], + createdAt: '2025-12-03T08:05:00Z', + updatedAt: '2026-02-02T09:20:00Z', + trending: true, + }, + { + id: 'skill-004', + name: 'test-optimizer', + displayName: 'Test Optimizer', + description: + 'Recommends missing tests and prioritizes flaky suites for stabilization.', + author: { + name: 'Jules Ng', + image: 'https://i.pravatar.cc/150?img=48', + isAgent: false, + }, + upvotes: 2540, + comments: 91, + installs: 11280, + category: 'Testing', + tags: ['jest', 'coverage', 'flaky'], + createdAt: '2025-09-12T13:00:00Z', + updatedAt: '2026-01-05T10:15:00Z', + trending: false, + }, + { + id: 'skill-005', + name: 'doc-polisher', + displayName: 'Doc Polisher', + description: + 'Transforms raw README notes into crisp onboarding guides and API docs.', + author: { + name: 'DocHound', + image: 'https://i.pravatar.cc/150?img=22', + isAgent: true, + }, + upvotes: 4130, + comments: 188, + installs: 25400, + category: 'Documentation', + tags: ['docs', 'markdown', 'onboarding'], + createdAt: '2025-08-25T11:30:00Z', + updatedAt: '2026-01-31T18:20:00Z', + trending: true, + }, + { + id: 'skill-006', + name: 'format-guardian', + displayName: 'Format Guardian', + description: + 'Applies opinionated formatting rules with a human-friendly explanation.', + author: { + name: 'Elena Garza', + image: 'https://i.pravatar.cc/150?img=9', + isAgent: false, + }, + upvotes: 1875, + comments: 64, + installs: 9020, + category: 'Formatting', + tags: ['prettier', 'lint', 'style'], + createdAt: '2025-12-14T10:00:00Z', + updatedAt: '2026-01-17T15:35:00Z', + trending: false, + }, + { + id: 'skill-007', + name: 'architecture-map', + displayName: 'Architecture Map', + description: + 'Builds a system map from repo structure and highlights ownership.', + author: { + name: 'Carter Lee', + image: 'https://i.pravatar.cc/150?img=52', + isAgent: false, + }, + upvotes: 2680, + comments: 102, + installs: 11980, + category: 'Architecture', + tags: ['systems', 'diagrams', 'ownership'], + createdAt: '2025-07-18T12:40:00Z', + updatedAt: '2025-12-22T09:50:00Z', + trending: false, + }, + { + id: 'skill-008', + name: 'security-sentry', + displayName: 'Security Sentry', + description: + 'Scans for risky dependencies and flags missing security headers.', + author: { + name: 'SecureScout', + image: 'https://i.pravatar.cc/150?img=35', + isAgent: true, + }, + upvotes: 3365, + comments: 142, + installs: 17890, + category: 'Security', + tags: ['deps', 'headers', 'vuln'], + createdAt: '2025-10-28T07:25:00Z', + updatedAt: '2026-01-26T13:45:00Z', + trending: true, + }, + { + id: 'skill-009', + name: 'api-guardian', + displayName: 'API Guardian', + description: + 'Checks for breaking API changes and generates migration notes.', + author: { + name: 'Priya Bose', + image: 'https://i.pravatar.cc/150?img=19', + isAgent: false, + }, + upvotes: 2210, + comments: 97, + installs: 10540, + category: 'API Design', + tags: ['openapi', 'versioning', 'breaking-changes'], + createdAt: '2025-06-30T10:10:00Z', + updatedAt: '2025-12-08T08:00:00Z', + trending: false, + }, + { + id: 'skill-010', + name: 'performance-pulse', + displayName: 'Performance Pulse', + description: + 'Summarizes profiler traces and suggests quick performance wins.', + author: { + name: 'PulseBot', + image: 'https://i.pravatar.cc/150?img=41', + isAgent: true, + }, + upvotes: 2955, + comments: 121, + installs: 15110, + category: 'Performance', + tags: ['profiling', 'latency', 'optimization'], + createdAt: '2025-11-22T06:35:00Z', + updatedAt: '2026-01-29T17:05:00Z', + trending: true, + }, + { + id: 'skill-011', + name: 'dx-pulse', + displayName: 'DX Pulse', + description: + 'Collects dev experience pain points and proposes quick fixes.', + author: { + name: 'Sana Iqbal', + image: 'https://i.pravatar.cc/150?img=14', + isAgent: false, + }, + upvotes: 1610, + comments: 54, + installs: 7880, + category: 'Productivity', + tags: ['dx', 'workflow', 'tooling'], + createdAt: '2026-01-12T09:05:00Z', + updatedAt: '2026-02-05T09:00:00Z', + trending: true, + }, + { + id: 'skill-012', + name: 'release-notes-pro', + displayName: 'Release Notes Pro', + description: + 'Turns merged PRs into polished release notes with highlights.', + author: { + name: 'ReleaseKit', + image: 'https://i.pravatar.cc/150?img=29', + isAgent: true, + }, + upvotes: 2080, + comments: 83, + installs: 9730, + category: 'Documentation', + tags: ['release', 'changelog', 'summaries'], + createdAt: '2025-12-21T15:40:00Z', + updatedAt: '2026-01-30T14:00:00Z', + trending: false, + }, + { + id: 'skill-013', + name: 'incident-brief', + displayName: 'Incident Brief', + description: + 'Builds concise incident summaries with timelines and remediation.', + author: { + name: 'Marco Ruiz', + image: 'https://i.pravatar.cc/150?img=6', + isAgent: false, + }, + upvotes: 1745, + comments: 61, + installs: 8040, + category: 'DevOps', + tags: ['incident', 'postmortem', 'ops'], + createdAt: '2026-01-04T07:25:00Z', + updatedAt: '2026-01-26T11:05:00Z', + trending: false, + }, + { + id: 'skill-014', + name: 'stack-matcher', + displayName: 'Stack Matcher', + description: + 'Suggests aligned toolchains based on repo conventions and team needs.', + author: { + name: 'Mira Chen', + image: 'https://i.pravatar.cc/150?img=17', + isAgent: false, + }, + upvotes: 1325, + comments: 39, + installs: 6120, + category: 'Architecture', + tags: ['stack', 'standards', 'tooling'], + createdAt: '2026-01-21T12:15:00Z', + updatedAt: '2026-02-03T10:50:00Z', + trending: false, + }, + { + id: 'skill-015', + name: 'ux-reviewer', + displayName: 'UX Reviewer', + description: + 'Flags accessibility gaps and UX inconsistencies in pull requests.', + author: { + name: 'A11yOrb', + image: 'https://i.pravatar.cc/150?img=39', + isAgent: true, + }, + upvotes: 2470, + comments: 118, + installs: 12490, + category: 'Design', + tags: ['a11y', 'ux', 'review'], + createdAt: '2025-10-09T10:50:00Z', + updatedAt: '2026-01-22T12:10:00Z', + trending: true, + }, + { + id: 'skill-016', + name: 'spec-writer', + displayName: 'Spec Writer', + description: + 'Turns meeting notes into crisp specs with user stories and risks.', + author: { + name: 'Noah Brooks', + image: 'https://i.pravatar.cc/150?img=21', + isAgent: false, + }, + upvotes: 1460, + comments: 47, + installs: 6880, + category: 'Planning', + tags: ['specs', 'product', 'requirements'], + createdAt: '2026-01-27T09:10:00Z', + updatedAt: '2026-02-06T08:55:00Z', + trending: true, + }, + { + id: 'skill-017', + name: 'monitoring-mentor', + displayName: 'Monitoring Mentor', + description: + 'Designs alerts and dashboards aligned to SLOs and incident history.', + author: { + name: 'Devon Price', + image: 'https://i.pravatar.cc/150?img=43', + isAgent: false, + }, + upvotes: 1565, + comments: 52, + installs: 7420, + category: 'Observability', + tags: ['slo', 'alerts', 'metrics'], + createdAt: '2025-11-10T11:05:00Z', + updatedAt: '2026-01-18T17:15:00Z', + trending: false, + }, + { + id: 'skill-018', + name: 'codebase-tour', + displayName: 'Codebase Tour', + description: + 'Generates onboarding walkthroughs with key folders and concepts.', + author: { + name: 'GuideLine', + image: 'https://i.pravatar.cc/150?img=27', + isAgent: true, + }, + upvotes: 1890, + comments: 76, + installs: 9540, + category: 'Onboarding', + tags: ['docs', 'learning', 'onboarding'], + createdAt: '2025-09-29T08:55:00Z', + updatedAt: '2026-01-14T09:45:00Z', + trending: false, + }, +]; diff --git a/packages/shared/src/features/skillHub/types.ts b/packages/shared/src/features/skillHub/types.ts new file mode 100644 index 0000000000..b1df6dad43 --- /dev/null +++ b/packages/shared/src/features/skillHub/types.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +export const skillAuthorSchema = z.object({ + name: z.string(), + image: z.string(), + isAgent: z.boolean(), +}); + +export const skillSchema = z.object({ + id: z.string(), + name: z.string(), + displayName: z.string(), + description: z.string(), + author: skillAuthorSchema, + upvotes: z.number(), + comments: z.number(), + installs: z.number(), + category: z.string(), + tags: z.array(z.string()), + createdAt: z.string(), + updatedAt: z.string(), + trending: z.boolean(), +}); + +export type Skill = z.infer; +export type SkillAuthor = z.infer; diff --git a/packages/webapp/pages/skills/index.tsx b/packages/webapp/pages/skills/index.tsx new file mode 100644 index 0000000000..cb1718b39b --- /dev/null +++ b/packages/webapp/pages/skills/index.tsx @@ -0,0 +1,61 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { NextSeoProps } from 'next-seo/lib/types'; +import { PageWrapperLayout } from '@dailydotdev/shared/src/components/layout/PageWrapperLayout'; +import { SkillHubHeader } from '@dailydotdev/shared/src/features/skillHub/components/SkillHubHeader'; +import { SkillRankingList } from '@dailydotdev/shared/src/features/skillHub/components/SkillRankingList'; +import { SkillGrid } from '@dailydotdev/shared/src/features/skillHub/components/SkillGrid'; +import { skillHubMockData } from '@dailydotdev/shared/src/features/skillHub/mocks'; +import type { Skill } from '@dailydotdev/shared/src/features/skillHub/types'; +import { getLayout } from '../../components/layouts/MainLayout'; +import { getLayout as getFooterNavBarLayout } from '../../components/layouts/FooterNavBarLayout'; +import { defaultOpenGraph } from '../../next-seo'; +import { getTemplatedTitle } from '../../components/layouts/utils'; + +const seo: NextSeoProps = { + title: getTemplatedTitle('Skill Hub'), + openGraph: { ...defaultOpenGraph }, + description: + 'Explore community-built skills for humans and agents. Discover top-ranked skills, trending workflows, and fresh experiments on daily.dev.', +}; + +const getRecentSkills = (skills: Skill[], limit: number): Skill[] => { + return [...skills] + .sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ) + .slice(0, limit); +}; + +const getTopSkills = (skills: Skill[], limit: number): Skill[] => { + return [...skills].sort((a, b) => b.upvotes - a.upvotes).slice(0, limit); +}; + +const SkillsPage = (): ReactElement => { + const topSkills = getTopSkills(skillHubMockData, 10); + const trendingSkills = skillHubMockData + .filter((skill) => skill.trending) + .slice(0, 9); + const recentSkills = getRecentSkills(skillHubMockData, 9); + + return ( + + + + + + + ); +}; + +const getSkillsPageLayout: typeof getLayout = (...props) => + getFooterNavBarLayout(getLayout(...props)); + +SkillsPage.getLayout = getSkillsPageLayout; +SkillsPage.layoutProps = { + screenCentered: false, + seo, +}; + +export default SkillsPage; From 783fab1f75223d1d22f62808e26a4b23c1a4eaf0 Mon Sep 17 00:00:00 2001 From: rebelchris Date: Tue, 10 Feb 2026 04:56:07 +0000 Subject: [PATCH 02/13] fix(skill-hub): simplify cards and ordering --- .../skillHub/components/SkillCard.tsx | 205 +++++++++--------- packages/webapp/pages/skills/index.tsx | 23 +- 2 files changed, 107 insertions(+), 121 deletions(-) diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx index 63c9c3ec14..e7f450e463 100644 --- a/packages/shared/src/features/skillHub/components/SkillCard.tsx +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -1,5 +1,5 @@ -import type { ReactElement, MouseEvent } from 'react'; -import React, { forwardRef } from 'react'; +import type { ReactElement } from 'react'; +import React from 'react'; import classNames from 'classnames'; import type { Skill } from '../types'; import { LazyImage } from '../../../components/LazyImage'; @@ -8,7 +8,6 @@ import { TypographyTag, TypographyType, } from '../../../components/typography/Typography'; -import Link from '../../../components/utilities/Link'; import { UpvoteIcon, DownloadIcon, DiscussIcon } from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; import { largeNumberFormat } from '../../../lib/numberFormat'; @@ -16,6 +15,7 @@ import { fallbackImages } from '../../../lib/config'; const NEW_SKILL_DAYS = 30; const DAY_IN_MS = 24 * 60 * 60 * 1000; +const MOCK_NOW = Date.parse('2026-02-10T00:00:00Z'); const isNewSkill = (createdAt: string): boolean => { const createdAtMs = Date.parse(createdAt); @@ -23,7 +23,7 @@ const isNewSkill = (createdAt: string): boolean => { return false; } - return Date.now() - createdAtMs <= NEW_SKILL_DAYS * DAY_IN_MS; + return MOCK_NOW - createdAtMs <= NEW_SKILL_DAYS * DAY_IN_MS; }; interface SkillCardProps { @@ -31,116 +31,109 @@ interface SkillCardProps { className?: string; } -export const SkillCard = forwardRef( - function SkillCardComponent({ skill, className }: SkillCardProps, ref): ReactElement { - const handleClick = (event: MouseEvent): void => { - event.preventDefault(); - }; +const formatCount = (value: number): string => { + return largeNumberFormat(value) || '0'; +}; - const formattedUpvotes = largeNumberFormat(skill.upvotes) || '0'; - const formattedComments = largeNumberFormat(skill.comments) || '0'; - const formattedInstalls = largeNumberFormat(skill.installs) || '0'; - const newBadge = isNewSkill(skill.createdAt); +export const SkillCard = ({ skill, className }: SkillCardProps): ReactElement => { + const newBadge = isNewSkill(skill.createdAt); - return ( - - +
+ + {skill.category} + + {newBadge && ( + + New + + )} +
+
+ + {skill.displayName} + + -
- - {skill.category} + {skill.description} + +
+ {skill.tags.slice(0, 3).map((tag) => ( + + {tag} - {newBadge && ( - - New - - )} -
-
+ ))} +
+
+
+
+ +
- {skill.displayName} + {skill.author.name} - - {skill.description} + {skill.author.isAgent ? 'Agent' : 'Human'} + +
+
+
+ + ); +}; diff --git a/packages/webapp/pages/skills/index.tsx b/packages/webapp/pages/skills/index.tsx index cb1718b39b..0612649afc 100644 --- a/packages/webapp/pages/skills/index.tsx +++ b/packages/webapp/pages/skills/index.tsx @@ -6,7 +6,6 @@ import { SkillHubHeader } from '@dailydotdev/shared/src/features/skillHub/compon import { SkillRankingList } from '@dailydotdev/shared/src/features/skillHub/components/SkillRankingList'; import { SkillGrid } from '@dailydotdev/shared/src/features/skillHub/components/SkillGrid'; import { skillHubMockData } from '@dailydotdev/shared/src/features/skillHub/mocks'; -import type { Skill } from '@dailydotdev/shared/src/features/skillHub/types'; import { getLayout } from '../../components/layouts/MainLayout'; import { getLayout as getFooterNavBarLayout } from '../../components/layouts/FooterNavBarLayout'; import { defaultOpenGraph } from '../../next-seo'; @@ -19,25 +18,19 @@ const seo: NextSeoProps = { 'Explore community-built skills for humans and agents. Discover top-ranked skills, trending workflows, and fresh experiments on daily.dev.', }; -const getRecentSkills = (skills: Skill[], limit: number): Skill[] => { - return [...skills] +const SkillsPage = (): ReactElement => { + const topSkills = [...skillHubMockData] + .sort((a, b) => b.upvotes - a.upvotes) + .slice(0, 10); + const trendingSkills = skillHubMockData + .filter((skill) => skill.trending) + .slice(0, 9); + const recentSkills = [...skillHubMockData] .sort( (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), ) - .slice(0, limit); -}; - -const getTopSkills = (skills: Skill[], limit: number): Skill[] => { - return [...skills].sort((a, b) => b.upvotes - a.upvotes).slice(0, limit); -}; - -const SkillsPage = (): ReactElement => { - const topSkills = getTopSkills(skillHubMockData, 10); - const trendingSkills = skillHubMockData - .filter((skill) => skill.trending) .slice(0, 9); - const recentSkills = getRecentSkills(skillHubMockData, 9); return ( From 1fbf40d9e2a47865ea4229e165c1bd4442fb05bd Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 05:41:49 +0000 Subject: [PATCH 03/13] fix(skill-hub): improve mobile layout and reduce overflow - Add horizontal padding (px-4) to main page container - Reduce header padding on mobile (p-4 -> tablet:p-6) - Optimize SkillCard padding and gaps for mobile (p-3/gap-3 -> tablet:p-4/gap-4) - Reduce section gaps on mobile (gap-6 -> tablet:gap-8) Co-authored-by: Chris Bongers --- .../src/features/skillHub/components/SkillCard.tsx | 13 ++++++++++--- .../features/skillHub/components/SkillHubHeader.tsx | 10 +++++++--- .../skillHub/components/SkillRankingList.tsx | 10 +++++----- packages/webapp/pages/skills/index.tsx | 2 +- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx index e7f450e463..36cd636910 100644 --- a/packages/shared/src/features/skillHub/components/SkillCard.tsx +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -8,7 +8,11 @@ import { TypographyTag, TypographyType, } from '../../../components/typography/Typography'; -import { UpvoteIcon, DownloadIcon, DiscussIcon } from '../../../components/icons'; +import { + UpvoteIcon, + DownloadIcon, + DiscussIcon, +} from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; import { largeNumberFormat } from '../../../lib/numberFormat'; import { fallbackImages } from '../../../lib/config'; @@ -35,7 +39,10 @@ const formatCount = (value: number): string => { return largeNumberFormat(value) || '0'; }; -export const SkillCard = ({ skill, className }: SkillCardProps): ReactElement => { +export const SkillCard = ({ + skill, + className, +}: SkillCardProps): ReactElement => { const newBadge = isNewSkill(skill.createdAt); return ( @@ -43,7 +50,7 @@ export const SkillCard = ({ skill, className }: SkillCardProps): ReactElement => type="button" aria-label={`Open ${skill.displayName}`} className={classNames( - 'group flex h-full w-full flex-col gap-4 rounded-16 border border-border-subtlest-tertiary bg-surface-float p-4 text-left transition', + 'group flex h-full w-full flex-col gap-3 rounded-16 border border-border-subtlest-tertiary bg-surface-float p-3 text-left transition tablet:gap-4 tablet:p-4', 'hover:border-border-subtlest-secondary hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2', 'focus-visible:ring-accent-bun-default focus-visible:ring-offset-2', className, diff --git a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx index 30c9b7bc30..633cca20f3 100644 --- a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx +++ b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx @@ -2,7 +2,11 @@ import type { ReactElement } from 'react'; import React from 'react'; import classNames from 'classnames'; import { SearchField } from '../../../components/fields/SearchField'; -import { Button, ButtonSize, ButtonVariant } from '../../../components/buttons/Button'; +import { + Button, + ButtonSize, + ButtonVariant, +} from '../../../components/buttons/Button'; import { PlusIcon, SparkleIcon } from '../../../components/icons'; import { Typography, @@ -24,8 +28,8 @@ const categories = [ export const SkillHubHeader = (): ReactElement => { return ( -
-
+
+
diff --git a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx index 567bd9290d..7109d73c6b 100644 --- a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx +++ b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx @@ -24,10 +24,7 @@ export const SkillRankingList = ({ className, }: SkillRankingListProps): ReactElement => { return ( - + {skills.map((skill, index) => (
- + {largeNumberFormat(skill.upvotes) || '0'}
diff --git a/packages/webapp/pages/skills/index.tsx b/packages/webapp/pages/skills/index.tsx index 0612649afc..8b977a9b0b 100644 --- a/packages/webapp/pages/skills/index.tsx +++ b/packages/webapp/pages/skills/index.tsx @@ -33,7 +33,7 @@ const SkillsPage = (): ReactElement => { .slice(0, 9); return ( - + From 1f56189a9122c028e89e17c4db15e70452214001 Mon Sep 17 00:00:00 2001 From: rebelchris Date: Tue, 10 Feb 2026 07:04:54 +0000 Subject: [PATCH 04/13] feat(skill-hub): add security callout --- .../skillHub/components/SkillHubHeader.tsx | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx index 633cca20f3..696d51aa1f 100644 --- a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx +++ b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx @@ -2,12 +2,14 @@ import type { ReactElement } from 'react'; import React from 'react'; import classNames from 'classnames'; import { SearchField } from '../../../components/fields/SearchField'; +import { Button, ButtonSize, ButtonVariant } from '../../../components/buttons/Button'; import { - Button, - ButtonSize, - ButtonVariant, -} from '../../../components/buttons/Button'; -import { PlusIcon, SparkleIcon } from '../../../components/icons'; + FlagIcon, + PlusIcon, + ShieldCheckIcon, + ShieldWarningIcon, + SparkleIcon, +} from '../../../components/icons'; import { Typography, TypographyTag, @@ -64,6 +66,31 @@ export const SkillHubHeader = (): ReactElement => { ))}
+
+ + Security layer + +
+
+ + + Verified publishers + +
+
+ + + Automated scans + +
+
+ + + Report suspicious skills + +
+
+
Date: Tue, 10 Feb 2026 07:05:00 +0000 Subject: [PATCH 05/13] docs(agents): add gh cli note --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index e562764e99..b7b7dfa7f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -383,6 +383,7 @@ const handleClick = useCallback((key: string) => { ## Pull Requests Keep PR descriptions concise and to the point. Reviewers should not be exhausted by lengthy explanations. +When using `gh pr create` from the shell, avoid unescaped backticks in the body (command substitution). Prefer single-quoted heredocs (`cat <<'EOF'`) or plain text without backticks. ## Code Review Guidelines From bd2122fac893775327088417ca92e4f86f0da880 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 10 Feb 2026 07:25:18 +0000 Subject: [PATCH 06/13] style(skill-hub): polish UI with gradients, hover states, and visual depth - Header: Add gradient glow effects, improved CTA with gradient button - Skill cards: Enhanced hover states with lift effect, category color coding - Ranking list: Medal-style rank badges with gold/silver/bronze for top 3 - Security callout: Refined with subtle avocado accent styling - Added 'Hot' badges for trending skills and improved author indicators - Better spacing and responsive layout --- .../skillHub/components/SkillCard.tsx | 150 +++++++++++--- .../skillHub/components/SkillGrid.tsx | 47 ++++- .../skillHub/components/SkillHubHeader.tsx | 190 +++++++++++------ .../skillHub/components/SkillRankingList.tsx | 196 ++++++++++++++---- packages/webapp/pages/skills/index.tsx | 6 +- 5 files changed, 444 insertions(+), 145 deletions(-) diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx index 36cd636910..ed17b2eefd 100644 --- a/packages/shared/src/features/skillHub/components/SkillCard.tsx +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -12,6 +12,7 @@ import { UpvoteIcon, DownloadIcon, DiscussIcon, + SparkleIcon, } from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; import { largeNumberFormat } from '../../../lib/numberFormat'; @@ -39,101 +40,188 @@ const formatCount = (value: number): string => { return largeNumberFormat(value) || '0'; }; +const getCategoryColor = ( + category: string, +): { bg: string; text: string; border: string } => { + const colors: Record = { + 'Code Review': { + bg: 'bg-accent-cabbage-subtlest', + text: 'text-accent-cabbage-default', + border: 'border-accent-cabbage-default/30', + }, + Database: { + bg: 'bg-accent-water-subtlest', + text: 'text-accent-water-default', + border: 'border-accent-water-default/30', + }, + DevOps: { + bg: 'bg-accent-onion-subtlest', + text: 'text-accent-onion-default', + border: 'border-accent-onion-default/30', + }, + Testing: { + bg: 'bg-accent-cheese-subtlest', + text: 'text-accent-cheese-default', + border: 'border-accent-cheese-default/30', + }, + Documentation: { + bg: 'bg-accent-blueCheese-subtlest', + text: 'text-accent-blueCheese-default', + border: 'border-accent-blueCheese-default/30', + }, + Security: { + bg: 'bg-accent-ketchup-subtlest', + text: 'text-accent-ketchup-default', + border: 'border-accent-ketchup-default/30', + }, + Design: { + bg: 'bg-accent-bacon-subtlest', + text: 'text-accent-bacon-default', + border: 'border-accent-bacon-default/30', + }, + }; + + return ( + colors[category] || { + bg: 'bg-surface-primary', + text: 'text-text-tertiary', + border: 'border-border-subtlest-tertiary', + } + ); +}; + export const SkillCard = ({ skill, className, }: SkillCardProps): ReactElement => { const newBadge = isNewSkill(skill.createdAt); + const categoryColor = getCategoryColor(skill.category); return ( +
{skills.map((skill) => ( diff --git a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx index 696d51aa1f..98f0786e5a 100644 --- a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx +++ b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx @@ -2,7 +2,11 @@ import type { ReactElement } from 'react'; import React from 'react'; import classNames from 'classnames'; import { SearchField } from '../../../components/fields/SearchField'; -import { Button, ButtonSize, ButtonVariant } from '../../../components/buttons/Button'; +import { + Button, + ButtonSize, + ButtonVariant, +} from '../../../components/buttons/Button'; import { FlagIcon, PlusIcon, @@ -30,81 +34,131 @@ const categories = [ export const SkillHubHeader = (): ReactElement => { return ( -
-
-
-
- - - -
- - Skill Hub - - - Discover, share, and discuss skills for humans and agents. - -
-
-
- {categories.map((category, index) => ( - - ))} -
-
- - Security layer - -
-
- - - Verified publishers +
+ {/* Gradient background glow */} +
+
+
+ +
+
+
+
+ + + + ✨ + + +
+ + Skill Hub -
-
- - - Automated scans + + Discover, share, and discuss skills for humans and agents.
+
+ + {/* Category pills */} +
+ {categories.map((category, index) => ( + + ))} +
+ + {/* Security callout - more subtle and elegant */} +
- - - Report suspicious skills + + + Security layer
+
+
+ + + Verified publishers + +
+
+ + + Automated scans + +
+
+ + + Report suspicious + +
+
-
-
- - + + {/* Search and CTA */} +
+ + +
diff --git a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx index 7109d73c6b..be6a403110 100644 --- a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx +++ b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx @@ -1,16 +1,17 @@ import type { ReactElement } from 'react'; import React from 'react'; +import classNames from 'classnames'; import type { Skill } from '../types'; -import { LeaderboardList } from '../../../components/cards/Leaderboard/LeaderboardList'; -import { LeaderboardListItem } from '../../../components/cards/Leaderboard/LeaderboardListItem'; +import { LazyImage } from '../../../components/LazyImage'; import { Typography, TypographyTag, TypographyType, } from '../../../components/typography/Typography'; -import { UpvoteIcon } from '../../../components/icons'; +import { UpvoteIcon, DownloadIcon, MedalBadgeIcon } from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; import { largeNumberFormat } from '../../../lib/numberFormat'; +import { fallbackImages } from '../../../lib/config'; interface SkillRankingListProps { title: string; @@ -18,49 +19,164 @@ interface SkillRankingListProps { className?: string; } +const getRankStyle = ( + index: number, +): { bg: string; text: string; border: string; glow?: string } => { + if (index === 0) { + return { + bg: 'bg-gradient-to-br from-accent-cheese-default to-accent-burger-default', + text: 'text-white', + border: 'border-accent-cheese-default/50', + glow: 'shadow-[0_0_12px_rgba(255,199,0,0.3)]', + }; + } + if (index === 1) { + return { + bg: 'bg-gradient-to-br from-accent-salt-subtle to-accent-salt-default', + text: 'text-white', + border: 'border-accent-salt-default/50', + glow: 'shadow-[0_0_8px_rgba(192,192,192,0.3)]', + }; + } + if (index === 2) { + return { + bg: 'bg-gradient-to-br from-accent-burger-subtle to-accent-burger-default', + text: 'text-white', + border: 'border-accent-burger-default/50', + glow: 'shadow-[0_0_8px_rgba(205,127,50,0.3)]', + }; + } + return { + bg: 'bg-surface-secondary', + text: 'text-text-tertiary', + border: 'border-border-subtlest-tertiary', + }; +}; + export const SkillRankingList = ({ title, skills, className, }: SkillRankingListProps): ReactElement => { return ( - - {skills.map((skill, index) => ( - -
-
- - {skill.displayName} - - - {skill.author.name} - -
-
- - +
+ + + {title} + +
+ +
+ {skills.map((skill, index) => { + const rankStyle = getRankStyle(index); + const isTopThree = index < 3; + + return ( +
- - ))} - + {index + 1} +
+ + {/* Author avatar */} +
+ + {skill.author.isAgent && ( + + 🤖 + + )} +
+ + {/* Skill info */} +
+ + {skill.displayName} + +
+ + by {skill.author.name} + + + + {skill.category} + +
+
+ + {/* Stats */} +
+
+ + + {largeNumberFormat(skill.installs) || '0'} + +
+
+ + + {largeNumberFormat(skill.upvotes) || '0'} + +
+
+ + ); + })} +
+
); }; diff --git a/packages/webapp/pages/skills/index.tsx b/packages/webapp/pages/skills/index.tsx index 8b977a9b0b..12e4c69ead 100644 --- a/packages/webapp/pages/skills/index.tsx +++ b/packages/webapp/pages/skills/index.tsx @@ -33,11 +33,11 @@ const SkillsPage = (): ReactElement => { .slice(0, 9); return ( - + - - + + ); }; From 5c3c291ca65df93e6eb24697c258c0f279c00fa8 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 10 Feb 2026 07:29:55 +0000 Subject: [PATCH 07/13] fix(skill-hub): eslint fixes - unused imports and formatting --- .../src/features/skillHub/components/SkillCard.tsx | 8 ++++---- .../features/skillHub/components/SkillHubHeader.tsx | 11 +++++------ .../skillHub/components/SkillRankingList.tsx | 13 ++++++++++--- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx index ed17b2eefd..9240f2e264 100644 --- a/packages/shared/src/features/skillHub/components/SkillCard.tsx +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -110,13 +110,13 @@ export const SkillCard = ({ )} > {/* Subtle gradient overlay on hover */} -
+
{/* Header row */}
( #{tag} @@ -181,7 +181,7 @@ export const SkillCard = ({ fallbackSrc={fallbackImages.avatar} /> {skill.author.isAgent && ( - + 🤖 )} diff --git a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx index 98f0786e5a..4c6473b54a 100644 --- a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx +++ b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx @@ -11,7 +11,6 @@ import { FlagIcon, PlusIcon, ShieldCheckIcon, - ShieldWarningIcon, SparkleIcon, } from '../../../components/icons'; import { @@ -36,9 +35,9 @@ export const SkillHubHeader = (): ReactElement => { return (
{/* Gradient background glow */} -
-
-
+
+
+
@@ -50,7 +49,7 @@ export const SkillHubHeader = (): ReactElement => { className="text-white" secondary /> - + @@ -93,7 +92,7 @@ export const SkillHubHeader = (): ReactElement => {
{/* Security callout - more subtle and elegant */} -
+
- + {title} @@ -108,7 +115,7 @@ export const SkillRankingList = ({ fallbackSrc={fallbackImages.avatar} /> {skill.author.isAgent && ( - + 🤖 )} From 8c8191c7c4a54dc813cd96c8323a40d376d8113d Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 07:42:42 +0000 Subject: [PATCH 08/13] fix(skill-hub): replace IconSize.Size12 with Size16 and fix formatting - Replace non-existent IconSize.Size12 with IconSize.Size16 in SkillCard and SkillHubHeader - Fix Prettier formatting in skills/index.tsx - Ensures ESLint passes and TypeScript compiles successfully Co-authored-by: Chris Bongers --- .../shared/src/features/skillHub/components/SkillCard.tsx | 2 +- .../src/features/skillHub/components/SkillHubHeader.tsx | 2 +- packages/webapp/pages/skills/index.tsx | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx index 9240f2e264..f9c3ff5e1d 100644 --- a/packages/shared/src/features/skillHub/components/SkillCard.tsx +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -127,7 +127,7 @@ export const SkillCard = ({
{skill.trending && ( - + Hot )} diff --git a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx index 4c6473b54a..9a3976e473 100644 --- a/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx +++ b/packages/shared/src/features/skillHub/components/SkillHubHeader.tsx @@ -128,7 +128,7 @@ export const SkillHubHeader = (): ReactElement => {
{ - + ); From 0060486e53777f1bd2ae2acab3bb34069159ff84 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 10 Feb 2026 08:38:57 +0000 Subject: [PATCH 09/13] fix(skill-hub): improve tag contrast and add skill detail modal --- .../skillHub/components/SkillCard.tsx | 5 +- .../skillHub/components/SkillDetailModal.tsx | 311 ++++++++++++++++++ .../skillHub/components/SkillGrid.tsx | 69 ++-- .../skillHub/components/SkillRankingList.tsx | 53 ++- 4 files changed, 398 insertions(+), 40 deletions(-) create mode 100644 packages/shared/src/features/skillHub/components/SkillDetailModal.tsx diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx index f9c3ff5e1d..65862bb7c5 100644 --- a/packages/shared/src/features/skillHub/components/SkillCard.tsx +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -34,6 +34,7 @@ const isNewSkill = (createdAt: string): boolean => { interface SkillCardProps { skill: Skill; className?: string; + onClick?: () => void; } const formatCount = (value: number): string => { @@ -93,6 +94,7 @@ const getCategoryColor = ( export const SkillCard = ({ skill, className, + onClick, }: SkillCardProps): ReactElement => { const newBadge = isNewSkill(skill.createdAt); const categoryColor = getCategoryColor(skill.category); @@ -101,6 +103,7 @@ export const SkillCard = ({ +
+ + {/* Author */} +
+
+ + {skill.author.isAgent && ( + + 🤖 + + )} +
+
+ + {skill.author.name} + + + {skill.author.isAgent ? 'AI Agent' : 'Human Creator'} + +
+
+
+ + {/* Content */} +
+ {/* Description */} +
+ + About + + + {skill.description} + +
+ + {/* Tags */} +
+ + Tags + +
+ {skill.tags.map((tag) => ( + + #{tag} + + ))} +
+
+ + {/* Stats */} +
+ + Stats + +
+
+ +
+ + {largeNumberFormat(skill.upvotes) || '0'} + + Upvotes +
+
+
+ +
+ + {largeNumberFormat(skill.comments) || '0'} + + Comments +
+
+
+ +
+ + {largeNumberFormat(skill.installs) || '0'} + + Installs +
+
+
+
+ + {/* Dates */} +
+
+ + + Created {formatDate(skill.createdAt)} + +
+
+ + + Updated {formatDate(skill.updatedAt)} + +
+
+
+ + {/* Footer */} +
+ + +
+
+ + ); +}; diff --git a/packages/shared/src/features/skillHub/components/SkillGrid.tsx b/packages/shared/src/features/skillHub/components/SkillGrid.tsx index 4128ffd9b2..6849074835 100644 --- a/packages/shared/src/features/skillHub/components/SkillGrid.tsx +++ b/packages/shared/src/features/skillHub/components/SkillGrid.tsx @@ -1,8 +1,9 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; import type { Skill } from '../types'; import { SkillCard } from './SkillCard'; +import { SkillDetailModal } from './SkillDetailModal'; import { Typography, TypographyTag, @@ -47,30 +48,52 @@ export const SkillGrid = ({ icon, }: SkillGridProps): ReactElement => { const iconElement = getIcon(icon); + const [selectedSkill, setSelectedSkill] = useState(null); + + const handleSkillClick = (skill: Skill) => { + setSelectedSkill(skill); + }; + + const handleCloseModal = () => { + setSelectedSkill(null); + }; return ( -
-
-
- {iconElement} - - {title} - + <> +
+
+
+ {iconElement} + + {title} + +
+ +
+
+ {skills.map((skill) => ( + handleSkillClick(skill)} + /> + ))}
- -
-
- {skills.map((skill) => ( - - ))} -
-
+
+ {selectedSkill && ( + + )} + ); }; diff --git a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx index 0d73cfefdb..d9a0cc052c 100644 --- a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx +++ b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx @@ -1,7 +1,8 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import classNames from 'classnames'; import type { Skill } from '../types'; +import { SkillDetailModal } from './SkillDetailModal'; import { LazyImage } from '../../../components/LazyImage'; import { Typography, @@ -62,20 +63,31 @@ export const SkillRankingList = ({ skills, className, }: SkillRankingListProps): ReactElement => { + const [selectedSkill, setSelectedSkill] = useState(null); + + const handleSkillClick = (skill: Skill) => { + setSelectedSkill(skill); + }; + + const handleCloseModal = () => { + setSelectedSkill(null); + }; + return ( -
-
- - - {title} - -
+ <> +
+
+ + + {title} + +
-
- {skills.map((skill, index) => { +
+ {skills.map((skill, index) => { const rankStyle = getRankStyle(index); const isTopThree = index < 3; @@ -84,6 +96,7 @@ export const SkillRankingList = ({ key={skill.id} type="button" aria-label={`View ${skill.displayName}`} + onClick={() => handleSkillClick(skill)} className={classNames( 'group flex w-full items-center gap-3 px-4 py-3 text-left transition-colors', 'hover:bg-surface-hover', @@ -166,7 +179,7 @@ export const SkillRankingList = ({ className={classNames( 'flex items-center gap-1.5 rounded-8 px-2 py-1', isTopThree - ? 'bg-accent-avocado-subtlest text-accent-avocado-default' + ? 'bg-accent-avocado-default text-white' : 'text-text-tertiary', )} > @@ -183,7 +196,15 @@ export const SkillRankingList = ({ ); })} -
-
+
+
+ {selectedSkill && ( + + )} + ); }; From 7d747823b6e7ad8820a8552c40f59b956162071f Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 10 Feb 2026 11:48:53 +0000 Subject: [PATCH 10/13] feat(skill-hub): replace modal with dedicated skill detail page - Created /skills/[id] page with full skill details - Shows long description, comments, repo link, license, version - Added mock comments data for realistic preview - Updated SkillCard and SkillRankingList to link to detail page - Removed SkillDetailModal in favor of the page approach - Extended Skill type with repoUrl, license, version, longDescription --- .../skillHub/components/SkillCard.tsx | 24 +- .../skillHub/components/SkillDetailModal.tsx | 311 ------------ .../skillHub/components/SkillGrid.tsx | 71 +-- .../skillHub/components/SkillRankingList.tsx | 60 +-- .../shared/src/features/skillHub/mocks.ts | 173 ++++++- .../shared/src/features/skillHub/types.ts | 13 + packages/webapp/pages/skills/[id]/index.tsx | 476 ++++++++++++++++++ 7 files changed, 716 insertions(+), 412 deletions(-) delete mode 100644 packages/shared/src/features/skillHub/components/SkillDetailModal.tsx create mode 100644 packages/webapp/pages/skills/[id]/index.tsx diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx index 65862bb7c5..5a0ed42d84 100644 --- a/packages/shared/src/features/skillHub/components/SkillCard.tsx +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -1,6 +1,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import classNames from 'classnames'; +import Link from '../../../components/utilities/Link'; import type { Skill } from '../types'; import { LazyImage } from '../../../components/LazyImage'; import { @@ -9,10 +10,10 @@ import { TypographyType, } from '../../../components/typography/Typography'; import { - UpvoteIcon, - DownloadIcon, DiscussIcon, + DownloadIcon, SparkleIcon, + UpvoteIcon, } from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; import { largeNumberFormat } from '../../../lib/numberFormat'; @@ -34,7 +35,6 @@ const isNewSkill = (createdAt: string): boolean => { interface SkillCardProps { skill: Skill; className?: string; - onClick?: () => void; } const formatCount = (value: number): string => { @@ -94,16 +94,14 @@ const getCategoryColor = ( export const SkillCard = ({ skill, className, - onClick, }: SkillCardProps): ReactElement => { const newBadge = isNewSkill(skill.createdAt); const categoryColor = getCategoryColor(skill.category); return ( - + ); }; diff --git a/packages/shared/src/features/skillHub/components/SkillDetailModal.tsx b/packages/shared/src/features/skillHub/components/SkillDetailModal.tsx deleted file mode 100644 index d11f4a437e..0000000000 --- a/packages/shared/src/features/skillHub/components/SkillDetailModal.tsx +++ /dev/null @@ -1,311 +0,0 @@ -import type { ReactElement } from 'react'; -import React from 'react'; -import classNames from 'classnames'; -import type { Skill } from '../types'; -import { Modal } from '../../../components/modals/common/Modal'; -import { ModalKind, ModalSize } from '../../../components/modals/common/types'; -import { LazyImage } from '../../../components/LazyImage'; -import { - Typography, - TypographyTag, - TypographyType, -} from '../../../components/typography/Typography'; -import { - UpvoteIcon, - DownloadIcon, - DiscussIcon, - CalendarIcon, -} from '../../../components/icons'; -import { IconSize } from '../../../components/Icon'; -import { - Button, - ButtonSize, - ButtonVariant, -} from '../../../components/buttons/Button'; -import { largeNumberFormat } from '../../../lib/numberFormat'; -import { fallbackImages } from '../../../lib/config'; - -interface SkillDetailModalProps { - skill: Skill; - isOpen: boolean; - onRequestClose: (e?: React.MouseEvent | React.KeyboardEvent) => void; -} - -const getCategoryColor = ( - category: string, -): { bg: string; text: string; border: string } => { - const colors: Record = { - 'Code Review': { - bg: 'bg-accent-cabbage-subtlest', - text: 'text-accent-cabbage-default', - border: 'border-accent-cabbage-default/30', - }, - Database: { - bg: 'bg-accent-water-subtlest', - text: 'text-accent-water-default', - border: 'border-accent-water-default/30', - }, - DevOps: { - bg: 'bg-accent-onion-subtlest', - text: 'text-accent-onion-default', - border: 'border-accent-onion-default/30', - }, - Testing: { - bg: 'bg-accent-cheese-subtlest', - text: 'text-accent-cheese-default', - border: 'border-accent-cheese-default/30', - }, - Documentation: { - bg: 'bg-accent-blueCheese-subtlest', - text: 'text-accent-blueCheese-default', - border: 'border-accent-blueCheese-default/30', - }, - Security: { - bg: 'bg-accent-ketchup-subtlest', - text: 'text-accent-ketchup-default', - border: 'border-accent-ketchup-default/30', - }, - Design: { - bg: 'bg-accent-bacon-subtlest', - text: 'text-accent-bacon-default', - border: 'border-accent-bacon-default/30', - }, - }; - - return ( - colors[category] || { - bg: 'bg-surface-primary', - text: 'text-text-tertiary', - border: 'border-border-subtlest-tertiary', - } - ); -}; - -const formatDate = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - }); -}; - -export const SkillDetailModal = ({ - skill, - isOpen, - onRequestClose, -}: SkillDetailModalProps): ReactElement => { - const categoryColor = getCategoryColor(skill.category); - - return ( - -
- {/* Header */} -
-
-
-
- - {skill.category} - - {skill.trending && ( - - 🔥 Trending - - )} -
- - {skill.displayName} - -
- -
- - {/* Author */} -
-
- - {skill.author.isAgent && ( - - 🤖 - - )} -
-
- - {skill.author.name} - - - {skill.author.isAgent ? 'AI Agent' : 'Human Creator'} - -
-
-
- - {/* Content */} -
- {/* Description */} -
- - About - - - {skill.description} - -
- - {/* Tags */} -
- - Tags - -
- {skill.tags.map((tag) => ( - - #{tag} - - ))} -
-
- - {/* Stats */} -
- - Stats - -
-
- -
- - {largeNumberFormat(skill.upvotes) || '0'} - - Upvotes -
-
-
- -
- - {largeNumberFormat(skill.comments) || '0'} - - Comments -
-
-
- -
- - {largeNumberFormat(skill.installs) || '0'} - - Installs -
-
-
-
- - {/* Dates */} -
-
- - - Created {formatDate(skill.createdAt)} - -
-
- - - Updated {formatDate(skill.updatedAt)} - -
-
-
- - {/* Footer */} -
- - -
-
-
- ); -}; diff --git a/packages/shared/src/features/skillHub/components/SkillGrid.tsx b/packages/shared/src/features/skillHub/components/SkillGrid.tsx index 6849074835..38c3a4efef 100644 --- a/packages/shared/src/features/skillHub/components/SkillGrid.tsx +++ b/packages/shared/src/features/skillHub/components/SkillGrid.tsx @@ -1,15 +1,14 @@ import type { ReactElement } from 'react'; -import React, { useState } from 'react'; +import React from 'react'; import classNames from 'classnames'; import type { Skill } from '../types'; import { SkillCard } from './SkillCard'; -import { SkillDetailModal } from './SkillDetailModal'; import { Typography, TypographyTag, TypographyType, } from '../../../components/typography/Typography'; -import { ArrowIcon, HotIcon, AddUserIcon } from '../../../components/icons'; +import { AddUserIcon, ArrowIcon, HotIcon } from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; import { Button, @@ -48,52 +47,30 @@ export const SkillGrid = ({ icon, }: SkillGridProps): ReactElement => { const iconElement = getIcon(icon); - const [selectedSkill, setSelectedSkill] = useState(null); - - const handleSkillClick = (skill: Skill) => { - setSelectedSkill(skill); - }; - - const handleCloseModal = () => { - setSelectedSkill(null); - }; return ( - <> -
-
-
- {iconElement} - - {title} - -
- -
-
- {skills.map((skill) => ( - handleSkillClick(skill)} - /> - ))} +
+
+
+ {iconElement} + + {title} +
-
- {selectedSkill && ( - - )} - + +
+
+ {skills.map((skill) => ( + + ))} +
+
); }; diff --git a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx index d9a0cc052c..fa65ce125b 100644 --- a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx +++ b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx @@ -1,8 +1,8 @@ import type { ReactElement } from 'react'; -import React, { useState } from 'react'; +import React from 'react'; import classNames from 'classnames'; +import Link from '../../../components/utilities/Link'; import type { Skill } from '../types'; -import { SkillDetailModal } from './SkillDetailModal'; import { LazyImage } from '../../../components/LazyImage'; import { Typography, @@ -10,9 +10,9 @@ import { TypographyType, } from '../../../components/typography/Typography'; import { - UpvoteIcon, DownloadIcon, MedalBadgeIcon, + UpvoteIcon, } from '../../../components/icons'; import { IconSize } from '../../../components/Icon'; import { largeNumberFormat } from '../../../lib/numberFormat'; @@ -63,40 +63,28 @@ export const SkillRankingList = ({ skills, className, }: SkillRankingListProps): ReactElement => { - const [selectedSkill, setSelectedSkill] = useState(null); - - const handleSkillClick = (skill: Skill) => { - setSelectedSkill(skill); - }; - - const handleCloseModal = () => { - setSelectedSkill(null); - }; - return ( - <> -
-
- - - {title} - -
+
+
+ + + {title} + +
-
- {skills.map((skill, index) => { +
+ {skills.map((skill, index) => { const rankStyle = getRankStyle(index); const isTopThree = index < 3; return ( -
- + ); })} -
-
- {selectedSkill && ( - - )} - +
+ ); }; diff --git a/packages/shared/src/features/skillHub/mocks.ts b/packages/shared/src/features/skillHub/mocks.ts index ab48fe9c01..e18b00e8f7 100644 --- a/packages/shared/src/features/skillHub/mocks.ts +++ b/packages/shared/src/features/skillHub/mocks.ts @@ -1,4 +1,4 @@ -import type { Skill } from './types'; +import type { Skill, SkillComment } from './types'; export const skillHubMockData: Skill[] = [ { @@ -7,6 +7,13 @@ export const skillHubMockData: Skill[] = [ displayName: 'Keyword Review', description: 'Highlights risky phrasing, missing scope, and ambiguous requirements before you ship.', + longDescription: `Keyword Review is designed to catch issues before they become problems. It scans your PRs and documents for: + +• **Risky phrasing** — Words like "simple", "just", or "obviously" that hide complexity +• **Missing scope** — Detects when requirements lack clear boundaries +• **Ambiguous language** — Flags terms that different team members might interpret differently + +Works with GitHub PRs, Notion docs, and Confluence pages. Integrates seamlessly into your existing workflow.`, author: { name: 'Riya Patel', image: 'https://i.pravatar.cc/150?img=12', @@ -20,6 +27,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-11-04T09:12:00Z', updatedAt: '2026-01-20T16:30:00Z', trending: true, + repoUrl: 'https://github.com/riyapatel/keyword-review', + license: 'MIT', + version: '2.4.1', }, { id: 'skill-002', @@ -27,6 +37,15 @@ export const skillHubMockData: Skill[] = [ displayName: 'Schema Guardian', description: 'Generates Prisma migrations with guardrails and explains breaking changes.', + longDescription: `Schema Guardian takes the fear out of database migrations. Built by an AI agent that's reviewed thousands of Prisma schemas. + +**Features:** +• Auto-generates safe migrations with rollback scripts +• Detects breaking changes before they hit production +• Explains migration impact in plain English +• Suggests index optimizations based on query patterns + +Supports PostgreSQL, MySQL, and SQLite. Works with Prisma 4.x and 5.x.`, author: { name: 'SchemaFox', image: 'https://i.pravatar.cc/150?img=32', @@ -40,6 +59,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-10-17T14:20:00Z', updatedAt: '2026-01-28T12:45:00Z', trending: true, + repoUrl: 'https://github.com/schema-fox/schema-guardian', + license: 'Apache-2.0', + version: '3.1.0', }, { id: 'skill-003', @@ -47,6 +69,15 @@ export const skillHubMockData: Skill[] = [ displayName: 'Deployment Diff', description: 'Summarizes infra diffs and flags risky Terraform changes before apply.', + longDescription: `Stop reviewing 500-line Terraform plans manually. Deployment Diff gives you a human-readable summary of what's actually changing. + +**What it catches:** +• Resource deletions that might cause downtime +• Security group changes that open new ports +• IAM policy modifications +• Cost impact estimates + +Integrates with Terraform Cloud, Spacelift, and GitHub Actions.`, author: { name: 'Ava Smith', image: 'https://i.pravatar.cc/150?img=5', @@ -60,6 +91,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-12-03T08:05:00Z', updatedAt: '2026-02-02T09:20:00Z', trending: true, + repoUrl: 'https://github.com/avasmith-dev/deployment-diff', + license: 'MIT', + version: '1.8.3', }, { id: 'skill-004', @@ -80,6 +114,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-09-12T13:00:00Z', updatedAt: '2026-01-05T10:15:00Z', trending: false, + repoUrl: 'https://github.com/julesng/test-optimizer', + license: 'MIT', + version: '1.2.0', }, { id: 'skill-005', @@ -100,6 +137,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-08-25T11:30:00Z', updatedAt: '2026-01-31T18:20:00Z', trending: true, + repoUrl: 'https://github.com/doc-hound/doc-polisher', + license: 'MIT', + version: '4.0.2', }, { id: 'skill-006', @@ -120,6 +160,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-12-14T10:00:00Z', updatedAt: '2026-01-17T15:35:00Z', trending: false, + repoUrl: 'https://github.com/elenagarza/format-guardian', + license: 'MIT', + version: '1.1.0', }, { id: 'skill-007', @@ -140,6 +183,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-07-18T12:40:00Z', updatedAt: '2025-12-22T09:50:00Z', trending: false, + repoUrl: 'https://github.com/carterlee/architecture-map', + license: 'Apache-2.0', + version: '2.0.0', }, { id: 'skill-008', @@ -160,6 +206,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-10-28T07:25:00Z', updatedAt: '2026-01-26T13:45:00Z', trending: true, + repoUrl: 'https://github.com/secure-scout/security-sentry', + license: 'MIT', + version: '3.2.1', }, { id: 'skill-009', @@ -180,6 +229,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-06-30T10:10:00Z', updatedAt: '2025-12-08T08:00:00Z', trending: false, + repoUrl: 'https://github.com/priyabose/api-guardian', + license: 'MIT', + version: '1.5.0', }, { id: 'skill-010', @@ -200,6 +252,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-11-22T06:35:00Z', updatedAt: '2026-01-29T17:05:00Z', trending: true, + repoUrl: 'https://github.com/pulse-bot/performance-pulse', + license: 'MIT', + version: '2.3.0', }, { id: 'skill-011', @@ -220,6 +275,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2026-01-12T09:05:00Z', updatedAt: '2026-02-05T09:00:00Z', trending: true, + repoUrl: 'https://github.com/sanaiqbal/dx-pulse', + license: 'MIT', + version: '1.0.3', }, { id: 'skill-012', @@ -240,6 +298,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-12-21T15:40:00Z', updatedAt: '2026-01-30T14:00:00Z', trending: false, + repoUrl: 'https://github.com/release-kit/release-notes-pro', + license: 'MIT', + version: '2.1.0', }, { id: 'skill-013', @@ -260,6 +321,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2026-01-04T07:25:00Z', updatedAt: '2026-01-26T11:05:00Z', trending: false, + repoUrl: 'https://github.com/marcoruiz/incident-brief', + license: 'Apache-2.0', + version: '1.3.0', }, { id: 'skill-014', @@ -280,6 +344,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2026-01-21T12:15:00Z', updatedAt: '2026-02-03T10:50:00Z', trending: false, + repoUrl: 'https://github.com/mirachen/stack-matcher', + license: 'MIT', + version: '1.0.0', }, { id: 'skill-015', @@ -300,6 +367,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-10-09T10:50:00Z', updatedAt: '2026-01-22T12:10:00Z', trending: true, + repoUrl: 'https://github.com/a11y-orb/ux-reviewer', + license: 'MIT', + version: '2.0.1', }, { id: 'skill-016', @@ -320,6 +390,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2026-01-27T09:10:00Z', updatedAt: '2026-02-06T08:55:00Z', trending: true, + repoUrl: 'https://github.com/noahbrooks/spec-writer', + license: 'MIT', + version: '1.1.0', }, { id: 'skill-017', @@ -340,6 +413,9 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-11-10T11:05:00Z', updatedAt: '2026-01-18T17:15:00Z', trending: false, + repoUrl: 'https://github.com/devonprice/monitoring-mentor', + license: 'MIT', + version: '1.4.2', }, { id: 'skill-018', @@ -360,5 +436,100 @@ export const skillHubMockData: Skill[] = [ createdAt: '2025-09-29T08:55:00Z', updatedAt: '2026-01-14T09:45:00Z', trending: false, + repoUrl: 'https://github.com/guide-line/codebase-tour', + license: 'MIT', + version: '1.6.0', }, ]; + +// Mock comments for skill detail pages +export const skillCommentsMockData: Record = { + 'skill-001': [ + { + id: 'comment-001', + content: + 'This saved us so much time during code review. The risky phrasing detection is spot on!', + author: { + name: 'Alex Kumar', + image: 'https://i.pravatar.cc/150?img=33', + isAgent: false, + }, + createdAt: '2026-01-15T14:30:00Z', + upvotes: 42, + }, + { + id: 'comment-002', + content: + 'Would love to see Slack integration. Currently copying the output manually.', + author: { + name: 'Sarah Chen', + image: 'https://i.pravatar.cc/150?img=44', + isAgent: false, + }, + createdAt: '2026-01-18T09:15:00Z', + upvotes: 28, + }, + { + id: 'comment-003', + content: + 'Been using this for 3 months. Highly recommend pairing with doc-polisher for complete coverage.', + author: { + name: 'CodeBot', + image: 'https://i.pravatar.cc/150?img=60', + isAgent: true, + }, + createdAt: '2026-01-20T11:45:00Z', + upvotes: 15, + }, + ], + 'skill-002': [ + { + id: 'comment-004', + content: + 'The breaking change detection alone is worth installing this. Prevented a production incident last week.', + author: { + name: 'Jordan Miles', + image: 'https://i.pravatar.cc/150?img=15', + isAgent: false, + }, + createdAt: '2026-01-25T16:20:00Z', + upvotes: 67, + }, + { + id: 'comment-005', + content: + 'Works great with Prisma 5.x. The rollback scripts are a lifesaver.', + author: { + name: 'Morgan Lee', + image: 'https://i.pravatar.cc/150?img=23', + isAgent: false, + }, + createdAt: '2026-01-27T08:30:00Z', + upvotes: 34, + }, + ], + 'skill-003': [ + { + id: 'comment-006', + content: + 'Finally, a tool that makes Terraform plan output readable. The cost estimates are super helpful.', + author: { + name: 'Casey Wong', + image: 'https://i.pravatar.cc/150?img=31', + isAgent: false, + }, + createdAt: '2026-02-01T10:00:00Z', + upvotes: 51, + }, + ], +}; + +// Helper to get skill by ID +export const getSkillById = (id: string): Skill | undefined => { + return skillHubMockData.find((skill) => skill.id === id); +}; + +// Helper to get comments for a skill +export const getSkillComments = (skillId: string): SkillComment[] => { + return skillCommentsMockData[skillId] || []; +}; diff --git a/packages/shared/src/features/skillHub/types.ts b/packages/shared/src/features/skillHub/types.ts index b1df6dad43..a440eacdd6 100644 --- a/packages/shared/src/features/skillHub/types.ts +++ b/packages/shared/src/features/skillHub/types.ts @@ -6,11 +6,20 @@ export const skillAuthorSchema = z.object({ isAgent: z.boolean(), }); +export const skillCommentSchema = z.object({ + id: z.string(), + content: z.string(), + author: skillAuthorSchema, + createdAt: z.string(), + upvotes: z.number(), +}); + export const skillSchema = z.object({ id: z.string(), name: z.string(), displayName: z.string(), description: z.string(), + longDescription: z.string().optional(), author: skillAuthorSchema, upvotes: z.number(), comments: z.number(), @@ -20,7 +29,11 @@ export const skillSchema = z.object({ createdAt: z.string(), updatedAt: z.string(), trending: z.boolean(), + repoUrl: z.string().optional(), + license: z.string().optional(), + version: z.string().optional(), }); export type Skill = z.infer; export type SkillAuthor = z.infer; +export type SkillComment = z.infer; diff --git a/packages/webapp/pages/skills/[id]/index.tsx b/packages/webapp/pages/skills/[id]/index.tsx new file mode 100644 index 0000000000..ddb5251aa4 --- /dev/null +++ b/packages/webapp/pages/skills/[id]/index.tsx @@ -0,0 +1,476 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import { useRouter } from 'next/router'; +import classNames from 'classnames'; +import { PageWrapperLayout } from '@dailydotdev/shared/src/components/layout/PageWrapperLayout'; +import { + Typography, + TypographyTag, + TypographyType, +} from '@dailydotdev/shared/src/components/typography/Typography'; +import { LazyImage } from '@dailydotdev/shared/src/components/LazyImage'; +import { + UpvoteIcon, + DownloadIcon, + DiscussIcon, + ArrowIcon, + GitHubIcon, +} from '@dailydotdev/shared/src/components/icons'; +import { IconSize } from '@dailydotdev/shared/src/components/Icon'; +import { + Button, + ButtonSize, + ButtonVariant, +} from '@dailydotdev/shared/src/components/buttons/Button'; +import { largeNumberFormat } from '@dailydotdev/shared/src/lib/numberFormat'; +import { fallbackImages } from '@dailydotdev/shared/src/lib/config'; +import { + getSkillById, + getSkillComments, +} from '@dailydotdev/shared/src/features/skillHub/mocks'; +import type { SkillComment } from '@dailydotdev/shared/src/features/skillHub/types'; +import { getLayout } from '../../../components/layouts/MainLayout'; +import { getLayout as getFooterNavBarLayout } from '../../../components/layouts/FooterNavBarLayout'; + +const getCategoryColor = ( + category: string, +): { bg: string; text: string; border: string } => { + const colors: Record = { + 'Code Review': { + bg: 'bg-accent-cabbage-subtlest', + text: 'text-accent-cabbage-default', + border: 'border-accent-cabbage-default/30', + }, + Database: { + bg: 'bg-accent-water-subtlest', + text: 'text-accent-water-default', + border: 'border-accent-water-default/30', + }, + DevOps: { + bg: 'bg-accent-onion-subtlest', + text: 'text-accent-onion-default', + border: 'border-accent-onion-default/30', + }, + Testing: { + bg: 'bg-accent-cheese-subtlest', + text: 'text-accent-cheese-default', + border: 'border-accent-cheese-default/30', + }, + Documentation: { + bg: 'bg-accent-blueCheese-subtlest', + text: 'text-accent-blueCheese-default', + border: 'border-accent-blueCheese-default/30', + }, + Security: { + bg: 'bg-accent-ketchup-subtlest', + text: 'text-accent-ketchup-default', + border: 'border-accent-ketchup-default/30', + }, + Design: { + bg: 'bg-accent-bacon-subtlest', + text: 'text-accent-bacon-default', + border: 'border-accent-bacon-default/30', + }, + }; + + return ( + colors[category] || { + bg: 'bg-surface-primary', + text: 'text-text-tertiary', + border: 'border-border-subtlest-tertiary', + } + ); +}; + +const formatDate = (dateString: string): string => { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +}; + +const formatRelativeTime = (dateString: string): string => { + const date = new Date(dateString); + const now = new Date('2026-02-10T00:00:00Z'); // Mock current date + const diffMs = now.getTime() - date.getTime(); + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays === 0) { + return 'Today'; + } + if (diffDays === 1) { + return 'Yesterday'; + } + if (diffDays < 7) { + return `${diffDays} days ago`; + } + if (diffDays < 30) { + return `${Math.floor(diffDays / 7)} weeks ago`; + } + if (diffDays < 365) { + return `${Math.floor(diffDays / 30)} months ago`; + } + return `${Math.floor(diffDays / 365)} years ago`; +}; + +interface CommentCardProps { + comment: SkillComment; +} + +const CommentCard = ({ comment }: CommentCardProps): ReactElement => ( +
+
+ + {comment.author.isAgent && ( + + 🤖 + + )} +
+
+
+ + {comment.author.name} + + + {formatRelativeTime(comment.createdAt)} + +
+ + {comment.content} + +
+ + {comment.upvotes} +
+
+
+); + +const SkillDetailPage = (): ReactElement => { + const router = useRouter(); + const { id } = router.query; + + const skill = getSkillById(id as string); + const comments = getSkillComments(id as string); + + if (!skill) { + return ( + + + Skill not found + + + + ); + } + + const categoryColor = getCategoryColor(skill.category); + + return ( + + {/* Back button */} + + +
+ {/* Main content */} +
+ {/* Header */} +
+
+ + {skill.category} + + {skill.trending && ( + + 🔥 Trending + + )} + {skill.version && ( + + v{skill.version} + + )} +
+ + + {skill.displayName} + + + + {skill.description} + + + {/* Author */} +
+
+ + {skill.author.isAgent && ( + + 🤖 + + )} +
+
+ + {skill.author.name} + + + {skill.author.isAgent ? 'AI Agent' : 'Human Creator'} + +
+
+ + {/* Stats */} +
+
+ +
+ + {largeNumberFormat(skill.upvotes) || '0'} + + Upvotes +
+
+
+ +
+ + {largeNumberFormat(skill.comments) || '0'} + + Comments +
+
+
+ +
+ + {largeNumberFormat(skill.installs) || '0'} + + Installs +
+
+
+
+ + {/* Long description */} + {skill.longDescription && ( +
+ + About + +
+ + {skill.longDescription} + +
+
+ )} + + {/* Comments section */} +
+
+ + Comments ({comments.length}) + + +
+ + {comments.length > 0 ? ( +
+ {comments.map((comment) => ( + + ))} +
+ ) : ( +
+ + + No comments yet. Be the first to share your thoughts! + +
+ )} +
+
+ + {/* Sidebar */} +
+ {/* Install card */} +
+ + + {skill.repoUrl && ( + + + View on GitHub + + )} +
+ + {/* Info card */} +
+ + Information + + +
+ {skill.license && ( +
+ + License + + {skill.license} +
+ )} +
+ + Created + + + {formatDate(skill.createdAt)} + +
+
+ + Updated + + + {formatDate(skill.updatedAt)} + +
+
+
+ + {/* Tags card */} +
+ + Tags + +
+ {skill.tags.map((tag) => ( + + #{tag} + + ))} +
+
+
+
+
+ ); +}; + +const getSkillsPageLayout: typeof getLayout = (...props) => + getFooterNavBarLayout(getLayout(...props)); + +SkillDetailPage.getLayout = getSkillsPageLayout; +SkillDetailPage.layoutProps = { + screenCentered: false, +}; + +export default SkillDetailPage; From 282292fbe12b7076d6f77d56469f04e3e415f23b Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 12:08:18 +0000 Subject: [PATCH 11/13] fix(skill-hub): wrap Link children in anchor tags for legacyBehavior The shared Link component uses Next.js legacyBehavior which requires children to be wrapped in an tag. The className and other props must be on the tag, not the Link component itself. This fixes TypeScript compilation errors in the build. Co-authored-by: Chris Bongers --- .../skillHub/components/SkillCard.tsx | 250 +++++++++--------- .../skillHub/components/SkillRankingList.tsx | 176 ++++++------ 2 files changed, 218 insertions(+), 208 deletions(-) diff --git a/packages/shared/src/features/skillHub/components/SkillCard.tsx b/packages/shared/src/features/skillHub/components/SkillCard.tsx index 5a0ed42d84..ed80f351ce 100644 --- a/packages/shared/src/features/skillHub/components/SkillCard.tsx +++ b/packages/shared/src/features/skillHub/components/SkillCard.tsx @@ -99,137 +99,147 @@ export const SkillCard = ({ const categoryColor = getCategoryColor(skill.category); return ( - - {/* Subtle gradient overlay on hover */} -
+ + + {/* Subtle gradient overlay on hover */} +
- {/* Header row */} -
- - {skill.category} - -
- {skill.trending && ( - - - Hot - - )} - {newBadge && ( - - New - - )} + {/* Header row */} +
+ + {skill.category} + +
+ {skill.trending && ( + + + Hot + + )} + {newBadge && ( + + New + + )} +
-
- {/* Main content */} -
- - {skill.displayName} - - - {skill.description} - - - {/* Tags */} -
- {skill.tags.slice(0, 3).map((tag) => ( - - #{tag} - - ))} -
-
+ {/* Main content */} +
+ + {skill.displayName} + + + {skill.description} + - {/* Footer */} -
- {/* Author */} -
-
- - {skill.author.isAgent && ( - - 🤖 + {/* Tags */} +
+ {skill.tags.slice(0, 3).map((tag) => ( + + #{tag} - )} -
-
- - {skill.author.name} - - - {skill.author.isAgent ? 'Agent' : 'Human'} - + ))}
- {/* Stats */} -
-
- - - {formatCount(skill.upvotes)} - -
-
- - - {formatCount(skill.comments)} - + {/* Footer */} +
+ {/* Author */} +
+
+ + {skill.author.isAgent && ( + + 🤖 + + )} +
+
+ + {skill.author.name} + + + {skill.author.isAgent ? 'Agent' : 'Human'} + +
-
- - - {formatCount(skill.installs)} - + + {/* Stats */} +
+
+ + + {formatCount(skill.upvotes)} + +
+
+ + + {formatCount(skill.comments)} + +
+
+ + + {formatCount(skill.installs)} + +
-
+
); }; diff --git a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx index fa65ce125b..19708848ad 100644 --- a/packages/shared/src/features/skillHub/components/SkillRankingList.tsx +++ b/packages/shared/src/features/skillHub/components/SkillRankingList.tsx @@ -81,106 +81,106 @@ export const SkillRankingList = ({ const isTopThree = index < 3; return ( - - {/* Rank badge */} - - - {/* Author avatar */} -
- - {skill.author.isAgent && ( - - 🤖 - - )} -
- - {/* Skill info */} -
- - {skill.displayName} - -
- - by {skill.author.name} - - - - {skill.category} - + {index + 1} +
+ + {/* Author avatar */} +
+ + {skill.author.isAgent && ( + + 🤖 + + )}
-
- {/* Stats */} -
-
- + {/* Skill info */} +
- {largeNumberFormat(skill.installs) || '0'} + {skill.displayName} +
+ + by {skill.author.name} + + + + {skill.category} + +
-
- - +
+ + + {largeNumberFormat(skill.installs) || '0'} + +
+
- {largeNumberFormat(skill.upvotes) || '0'} - + + + {largeNumberFormat(skill.upvotes) || '0'} + +
-
+
); })} From f9f9a243db3f6d7f3d995326ca74ae08bae18276 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 10 Feb 2026 12:20:01 +0000 Subject: [PATCH 12/13] refactor(skill-hub): align skill detail page with post page format Updated skill detail page to match the post page structure: - Uses same container layout (max-w-[69.25rem], border-x on laptop) - PageWidgets sidebar with author card, install card, info card - PostSourceInfo-style author header with avatar and timestamp - PostTagList-style tags with category badge - PostMetadata-style info row (installs, version, license, source link) - PostComments-style comment section with proper comment cards - Action bar similar to post actions (upvote, comment, install) - Same spacing and typography patterns as post pages --- packages/webapp/pages/skills/[id]/index.tsx | 597 ++++++++++---------- 1 file changed, 307 insertions(+), 290 deletions(-) diff --git a/packages/webapp/pages/skills/[id]/index.tsx b/packages/webapp/pages/skills/[id]/index.tsx index ddb5251aa4..6306ff96d9 100644 --- a/packages/webapp/pages/skills/[id]/index.tsx +++ b/packages/webapp/pages/skills/[id]/index.tsx @@ -2,7 +2,6 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useRouter } from 'next/router'; import classNames from 'classnames'; -import { PageWrapperLayout } from '@dailydotdev/shared/src/components/layout/PageWrapperLayout'; import { Typography, TypographyTag, @@ -10,11 +9,11 @@ import { } from '@dailydotdev/shared/src/components/typography/Typography'; import { LazyImage } from '@dailydotdev/shared/src/components/LazyImage'; import { - UpvoteIcon, - DownloadIcon, - DiscussIcon, ArrowIcon, + DiscussIcon, + DownloadIcon, GitHubIcon, + UpvoteIcon, } from '@dailydotdev/shared/src/components/icons'; import { IconSize } from '@dailydotdev/shared/src/components/Icon'; import { @@ -24,6 +23,8 @@ import { } from '@dailydotdev/shared/src/components/buttons/Button'; import { largeNumberFormat } from '@dailydotdev/shared/src/lib/numberFormat'; import { fallbackImages } from '@dailydotdev/shared/src/lib/config'; +import { PageWidgets } from '@dailydotdev/shared/src/components/utilities'; +import Markdown from '@dailydotdev/shared/src/components/Markdown'; import { getSkillById, getSkillComments, @@ -93,7 +94,7 @@ const formatDate = (dateString: string): string => { const formatRelativeTime = (dateString: string): string => { const date = new Date(dateString); - const now = new Date('2026-02-10T00:00:00Z'); // Mock current date + const now = new Date('2026-02-10T00:00:00Z'); const diffMs = now.getTime() - date.getTime(); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); @@ -104,58 +105,83 @@ const formatRelativeTime = (dateString: string): string => { return 'Yesterday'; } if (diffDays < 7) { - return `${diffDays} days ago`; + return `${diffDays}d`; } if (diffDays < 30) { - return `${Math.floor(diffDays / 7)} weeks ago`; + return `${Math.floor(diffDays / 7)}w`; } if (diffDays < 365) { - return `${Math.floor(diffDays / 30)} months ago`; + return `${Math.floor(diffDays / 30)}mo`; } - return `${Math.floor(diffDays / 365)} years ago`; + return `${Math.floor(diffDays / 365)}y`; }; -interface CommentCardProps { +interface CommentItemProps { comment: SkillComment; } -const CommentCard = ({ comment }: CommentCardProps): ReactElement => ( -
-
- - {comment.author.isAgent && ( - - 🤖 - - )} -
-
-
- - {comment.author.name} - - +const CommentItem = ({ comment }: CommentItemProps): ReactElement => ( +
+
+
+ + {comment.author.isAgent && ( + + 🤖 + + )} +
+
+
+ + {comment.author.name} + + {comment.author.isAgent && ( + + Agent + + )} +
+ {formatRelativeTime(comment.createdAt)}
+
+
{comment.content} -
- - {comment.upvotes} -
-
+
+ + +
+ ); const SkillDetailPage = (): ReactElement => { @@ -167,7 +193,7 @@ const SkillDetailPage = (): ReactElement => { if (!skill) { return ( - +
Skill not found @@ -177,291 +203,282 @@ const SkillDetailPage = (): ReactElement => { > Back to Skill Hub - +
); } const categoryColor = getCategoryColor(skill.category); return ( - - {/* Back button */} - +
+ {/* Main content */} +
+ {/* Back navigation */} + -
- {/* Main content */} -
- {/* Header */} -
-
- - {skill.category} + {/* Skill source info (like PostSourceInfo) */} +
+
+ + {skill.author.isAgent && ( + + 🤖 - {skill.trending && ( - - 🔥 Trending - - )} - {skill.version && ( - - v{skill.version} - - )} -
- - - {skill.displayName} - - + )} +
+
- {skill.description} + {skill.author.name} - - {/* Author */} -
-
- - {skill.author.isAgent && ( - - 🤖 - - )} -
-
- - {skill.author.name} - - - {skill.author.isAgent ? 'AI Agent' : 'Human Creator'} - -
-
- - {/* Stats */} -
-
- -
- - {largeNumberFormat(skill.upvotes) || '0'} - - Upvotes -
-
-
- -
- - {largeNumberFormat(skill.comments) || '0'} - - Comments -
-
-
- -
- - {largeNumberFormat(skill.installs) || '0'} - - Installs -
-
-
+ + + {formatRelativeTime(skill.createdAt)} +
+
- {/* Long description */} - {skill.longDescription && ( -
- - About - -
- - {skill.longDescription} - -
-
- )} + {/* Title */} +

+ {skill.displayName} +

- {/* Comments section */} -
-
- - Comments ({comments.length}) - - -
- - {comments.length > 0 ? ( -
- {comments.map((comment) => ( - - ))} -
- ) : ( -
- - - No comments yet. Be the first to share your thoughts! - -
+ {/* Tags (like PostTagList) */} +
+ -
- - {/* Sidebar */} -
- {/* Install card */} -
- + #{tag} + + ))} +
- {skill.repoUrl && ( + {/* Metadata (like PostMetadata) */} +
+ {largeNumberFormat(skill.installs)} installs + + v{skill.version || '1.0.0'} + + {skill.license || 'MIT'} + {skill.repoUrl && ( + <> + - - View on GitHub + + Source - )} -
+ + )} +
- {/* Info card */} -
- - Information - + {/* Description content */} +
+ + {skill.description} + + {skill.longDescription && ( +
+ +
+ )} +
-
- {skill.license && ( -
- - License - - {skill.license} -
- )} -
- - Created - - - {formatDate(skill.createdAt)} - -
-
- - Updated - - - {formatDate(skill.updatedAt)} + {/* Action buttons (like post actions) */} +
+ + + +
+ + {/* Comments section (like PostComments) */} +
+ + Comments ({comments.length}) + + + {comments.length > 0 ? ( +
+ {comments.map((comment) => ( + + ))} +
+ ) : ( +
+ Be the first to comment. +
+ )} +
+
+ + {/* Sidebar (like PostWidgets) */} + + {/* Author card */} +
+
+
+ + {skill.author.isAgent && ( + + 🤖 -
+ )} +
+
+ + {skill.author.name} + + + {skill.author.isAgent ? 'AI Agent Creator' : 'Human Creator'} +
+ +
- {/* Tags card */} -
- + + Install this skill + + + {skill.repoUrl && ( + - Tags - - + + {/* Info card */} +
+ + Information + +
+
+ Version + + {skill.version || '1.0.0'} + +
+
+ License + + {skill.license || 'MIT'} + +
+
+ Created + + {formatDate(skill.createdAt)} + +
+
+ Updated + + {formatDate(skill.updatedAt)} +
-
-
+ +
); }; From 68df3a55d66b90a640c0a44003ee3970d1e44fde Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 10 Feb 2026 12:30:20 +0000 Subject: [PATCH 13/13] refactor(skill-hub): match daily.dev design patterns Overview page: - Uses BaseFeedPage with gradient background like squads directory - Tab navigation (Featured, Trending, New, Top) like explore tabs - FeedContainer for skill cards grid - Proper header with search and CTA matching other pages Detail page: - Mobile back header like GoBackHeaderMobile - PostSourceInfo-style author header - PostTagList-style tags - PostMetadata-style info line - PostActions-style action bar with QuaternaryButtons - PostComments-style comment section - PostWidgets sidebar layout --- packages/webapp/pages/skills/[id]/index.tsx | 199 ++++++++++--------- packages/webapp/pages/skills/index.tsx | 206 ++++++++++++++++---- 2 files changed, 275 insertions(+), 130 deletions(-) diff --git a/packages/webapp/pages/skills/[id]/index.tsx b/packages/webapp/pages/skills/[id]/index.tsx index 6306ff96d9..ca979cffdf 100644 --- a/packages/webapp/pages/skills/[id]/index.tsx +++ b/packages/webapp/pages/skills/[id]/index.tsx @@ -10,9 +10,12 @@ import { import { LazyImage } from '@dailydotdev/shared/src/components/LazyImage'; import { ArrowIcon, + BookmarkIcon, DiscussIcon, DownloadIcon, GitHubIcon, + LinkIcon, + ShareIcon, UpvoteIcon, } from '@dailydotdev/shared/src/components/icons'; import { IconSize } from '@dailydotdev/shared/src/components/Icon'; @@ -21,6 +24,7 @@ import { ButtonSize, ButtonVariant, } from '@dailydotdev/shared/src/components/buttons/Button'; +import { QuaternaryButton } from '@dailydotdev/shared/src/components/buttons/QuaternaryButton'; import { largeNumberFormat } from '@dailydotdev/shared/src/lib/numberFormat'; import { fallbackImages } from '@dailydotdev/shared/src/lib/config'; import { PageWidgets } from '@dailydotdev/shared/src/components/utilities'; @@ -121,7 +125,7 @@ interface CommentItemProps { } const CommentItem = ({ comment }: CommentItemProps): ReactElement => ( -
+
( Agent )} + + · {formatRelativeTime(comment.createdAt)} +
- - {formatRelativeTime(comment.createdAt)} -
-
+
( > {comment.content} -
-
- - +
+ + + {comment.upvotes} + + + + Reply + +
); @@ -211,58 +209,67 @@ const SkillDetailPage = (): ReactElement => { return (
- {/* Main content */} + {/* Main content - matching PostContent structure */}
- {/* Back navigation */} - + {/* Mobile back header */} +
+ +
+ + + + + + +
+
- {/* Skill source info (like PostSourceInfo) */} -
+ {/* Post source info style header */} +
{skill.author.isAgent && ( - + 🤖 )}
-
+
{skill.author.name} - {formatRelativeTime(skill.createdAt)}
- {/* Title */} + {/* Title - like post title */}

{skill.displayName}

- {/* Tags (like PostTagList) */} + {/* Tags - like PostTagList */}
{ ))}
- {/* Metadata (like PostMetadata) */} -
+ {/* Metadata - like PostMetadata */} +
{largeNumberFormat(skill.installs)} installs - + v{skill.version || '1.0.0'} - + {skill.license || 'MIT'} {skill.repoUrl && ( <> - + { className="flex items-center gap-1 hover:text-text-primary hover:underline" > - Source + View source )}
- {/* Description content */} + {/* Content */}
{ )}
- {/* Action buttons (like post actions) */} -
- - + {/* Post actions bar - like PostActions */} +
+
+ + + {largeNumberFormat(skill.upvotes)} + + + + {largeNumberFormat(skill.comments)} + + + + + + + +
- {/* Comments section (like PostComments) */} + {/* Comments section - like PostComments */}
- - Comments ({comments.length}) - +
+ + Comments + +
{comments.length > 0 ? ( -
+
{comments.map((comment) => ( ))} @@ -372,14 +377,14 @@ const SkillDetailPage = (): ReactElement => {
- {/* Sidebar (like PostWidgets) */} + {/* Sidebar - like PostWidgets */} - {/* Author card */} -
+ {/* Author card - like SourceEntityCard */} +
{ : 'text-text-quaternary', )} > - {skill.author.isAgent ? 'AI Agent Creator' : 'Human Creator'} + {skill.author.isAgent ? 'AI Agent' : 'Skill Creator'}
@@ -419,11 +424,8 @@ const SkillDetailPage = (): ReactElement => {
- {/* Install card */} -
- - Install this skill - + {/* Install CTA */} +
{/* Info card */} -
- - Information +
+ + ABOUT -
+
- Version + Version {skill.version || '1.0.0'}
- License + License {skill.license || 'MIT'}
- Created + Created {formatDate(skill.createdAt)}
- Updated + Updated {formatDate(skill.updatedAt)} diff --git a/packages/webapp/pages/skills/index.tsx b/packages/webapp/pages/skills/index.tsx index a80ae49b6a..bcd5810d1c 100644 --- a/packages/webapp/pages/skills/index.tsx +++ b/packages/webapp/pages/skills/index.tsx @@ -1,13 +1,33 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useState } from 'react'; import type { NextSeoProps } from 'next-seo/lib/types'; -import { PageWrapperLayout } from '@dailydotdev/shared/src/components/layout/PageWrapperLayout'; -import { SkillHubHeader } from '@dailydotdev/shared/src/features/skillHub/components/SkillHubHeader'; -import { SkillRankingList } from '@dailydotdev/shared/src/features/skillHub/components/SkillRankingList'; -import { SkillGrid } from '@dailydotdev/shared/src/features/skillHub/components/SkillGrid'; +import classNames from 'classnames'; +import { BaseFeedPage } from '@dailydotdev/shared/src/components/utilities'; +import { FeedContainer } from '@dailydotdev/shared/src/components/feeds'; +import { + Button, + ButtonSize, + ButtonVariant, +} from '@dailydotdev/shared/src/components/buttons/Button'; +import { SearchField } from '@dailydotdev/shared/src/components/fields/SearchField'; +import { + Typography, + TypographyTag, + TypographyType, +} from '@dailydotdev/shared/src/components/typography/Typography'; +import { + HotIcon, + PlusIcon, + SparkleIcon, + AddUserIcon, + MedalBadgeIcon, +} from '@dailydotdev/shared/src/components/icons'; +import { IconSize } from '@dailydotdev/shared/src/components/Icon'; +import { SkillCard } from '@dailydotdev/shared/src/features/skillHub/components/SkillCard'; import { skillHubMockData } from '@dailydotdev/shared/src/features/skillHub/mocks'; -import { getLayout } from '../../components/layouts/MainLayout'; -import { getLayout as getFooterNavBarLayout } from '../../components/layouts/FooterNavBarLayout'; +import CustomAuthBanner from '@dailydotdev/shared/src/components/auth/CustomAuthBanner'; +import { getLayout } from '../../components/layouts/FeedLayout'; +import { mainFeedLayoutProps } from '../../components/layouts/MainFeedPage'; import { defaultOpenGraph } from '../../next-seo'; import { getTemplatedTitle } from '../../components/layouts/utils'; @@ -18,40 +38,158 @@ const seo: NextSeoProps = { 'Explore community-built skills for humans and agents. Discover top-ranked skills, trending workflows, and fresh experiments on daily.dev.', }; +type TabType = 'featured' | 'trending' | 'new' | 'top'; + +const tabs: { id: TabType; label: string; icon: ReactElement }[] = [ + { + id: 'featured', + label: 'Featured', + icon: , + }, + { + id: 'trending', + label: 'Trending', + icon: , + }, + { id: 'new', label: 'New', icon: }, + { id: 'top', label: 'Top', icon: }, +]; + const SkillsPage = (): ReactElement => { - const topSkills = [...skillHubMockData] - .sort((a, b) => b.upvotes - a.upvotes) - .slice(0, 10); - const trendingSkills = skillHubMockData - .filter((skill) => skill.trending) - .slice(0, 9); - const recentSkills = [...skillHubMockData] - .sort( - (a, b) => - new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), - ) - .slice(0, 9); + const [activeTab, setActiveTab] = useState('featured'); + + const getFilteredSkills = () => { + switch (activeTab) { + case 'trending': + return skillHubMockData.filter((skill) => skill.trending); + case 'new': + return [...skillHubMockData].sort( + (a, b) => + new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); + case 'top': + return [...skillHubMockData].sort((a, b) => b.upvotes - a.upvotes); + case 'featured': + default: + return skillHubMockData.filter((skill) => skill.trending); + } + }; + + const skills = getFilteredSkills(); return ( - - - - - - + + {/* Gradient background like squads */} +
+ + {/* Header */} +
+ {/* Mobile header */} +
+
+ + + Skill Hub + +
+ +
+ + {/* Desktop header */} +
+
+
+ + + +
+ + Skill Hub + + + Discover, share, and discuss skills for humans and agents. + +
+
+
+
+ + +
+
+ + {/* Tabs */} + +
+ + {/* Skills grid using FeedContainer */} + + {skills.map((skill) => ( + + ))} + + ); }; -const getSkillsPageLayout: typeof getLayout = (...props) => - getFooterNavBarLayout(getLayout(...props)); - -SkillsPage.getLayout = getSkillsPageLayout; +SkillsPage.getLayout = getLayout; SkillsPage.layoutProps = { - screenCentered: false, + ...mainFeedLayoutProps, + customBanner: , seo, };