cfahlgren1 HF staff commited on
Commit
297c500
β€’
1 Parent(s): 5053808

dockerize, add category pages, and footer

Browse files
Dockerfile ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1.4
2
+
3
+ FROM node:20-alpine AS base
4
+
5
+ # Install dependencies only when needed
6
+ FROM base AS deps
7
+ WORKDIR /app
8
+
9
+ # Install dependencies based on the preferred package manager
10
+ COPY package.json package-lock.json* ./
11
+ RUN npm ci
12
+
13
+ # Rebuild the source code only when needed
14
+ FROM base AS builder
15
+ WORKDIR /app
16
+ COPY --from=deps /app/node_modules ./node_modules
17
+ COPY . .
18
+
19
+ # Next.js collects completely anonymous telemetry data about general usage.
20
+ # Uncomment the following line in case you want to disable telemetry during the build.
21
+ # ENV NEXT_TELEMETRY_DISABLED 1
22
+
23
+ RUN npm run build
24
+
25
+ # Production image, copy all the files and run next
26
+ FROM base AS runner
27
+ WORKDIR /app
28
+
29
+ ENV NODE_ENV production
30
+ # Uncomment the following line in case you want to disable telemetry during runtime.
31
+ # ENV NEXT_TELEMETRY_DISABLED 1
32
+
33
+ RUN \
34
+ addgroup --system --gid 1001 nodejs; \
35
+ adduser --system --uid 1001 nextjs
36
+
37
+ COPY --from=builder /app/public ./public
38
+
39
+ # Automatically leverage output traces to reduce image size
40
+ COPY --from=builder --chown=1001:1001 /app/.next/standalone ./
41
+ COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static
42
+
43
+ USER nextjs
44
+
45
+ EXPOSE 3000
46
+
47
+ ENV PORT 3000
48
+ ENV HOSTNAME 0.0.0.0
49
+ CMD ["node", "server.js"]
app/category/[slug]/page.tsx ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import CategoryContent from '@/components/category/CategoryContent';
2
+
3
+ export default function CategoryPage({ params }: { params: { slug: string } }) {
4
+ return (
5
+ <div className="container mx-auto px-4 py-8">
6
+ <CategoryContent slug={params.slug} />
7
+ </div>
8
+ );
9
+ }
app/page.tsx CHANGED
@@ -1,6 +1,8 @@
1
  "use client"
2
  import PillLink from '@/components/PillLink';
3
- import CategoryRow from '@/components/CategoryRow';
 
 
4
  import { categories } from "@/lib/categories";
5
 
6
  export default function Home() {
@@ -10,12 +12,18 @@ export default function Home() {
10
  <h1 className="text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tighter mb-4">
11
  Playground
12
  </h1>
13
- <p className="font-bold text-slate-700 font-mono italic text-lg mb-10">
14
  The fastest way to get started with Transformers.js.
15
  </p>
 
 
 
16
  <div className="mt-24">
17
  <CategoryRow categories={categories} />
18
  </div>
 
 
 
19
  </div>
20
  );
21
  }
 
1
  "use client"
2
  import PillLink from '@/components/PillLink';
3
+ import CategoryRow from '@/components/category/CategoryRow';
4
+ import InstallLine from '@/components/InstallLine';
5
+ import Footer from '@/components/Footer';
6
  import { categories } from "@/lib/categories";
7
 
8
  export default function Home() {
 
12
  <h1 className="text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tighter mb-4">
13
  Playground
14
  </h1>
15
+ <p className="font-bold text-slate-700 font-mono italic text-lg mb-6">
16
  The fastest way to get started with Transformers.js.
17
  </p>
18
+ <div className="mx-auto mb-12">
19
+ <InstallLine packageName="@huggingface/transformers" />
20
+ </div>
21
  <div className="mt-24">
22
  <CategoryRow categories={categories} />
23
  </div>
24
+ <div className="mt-64">
25
+ <Footer />
26
+ </div>
27
  </div>
28
  );
29
  }
components/Footer.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import Link from 'next/link';
3
+ import { categories } from '../lib/categories';
4
+
5
+ const Footer: React.FC = () => {
6
+ return (
7
+ <footer className="bg-emerald-400/80 p-20 relative overflow-hidden rounded-xl">
8
+ <div className="max-w-7xl mx-auto relative z-10">
9
+ <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-8 mb-12">
10
+ {categories.map((category) => (
11
+ <div key={category.slug} className="text-center sm:text-left">
12
+ <Link
13
+ href={`/category/${category.slug}`}
14
+ className="text-lg font-bold text-emerald-800 hover:text-emerald-900 transition-colors duration-300 ease-in-out relative group"
15
+ >
16
+ {category.title}
17
+ <span className="absolute left-0 right-0 bottom-0 h-0.5 bg-emerald-800 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out"></span>
18
+ </Link>
19
+ </div>
20
+ ))}
21
+ </div>
22
+ </div>
23
+
24
+ <div className="absolute inset-x-0 bottom-0 text-center py-4">
25
+ <h1 className="text-6xl md:text-8xl font-black text-red-500 tracking-tighter transform -skew-x-6">
26
+ TRANSFORMERS.JS
27
+ </h1>
28
+ </div>
29
+
30
+ <div className="absolute inset-0 flex items-center justify-center opacity-5">
31
+ <div className="w-96 h-96 bg-black rounded-full"></div>
32
+ </div>
33
+ </footer>
34
+ );
35
+ };
36
+
37
+ export default Footer;
components/InstallLine.tsx ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react';
2
+ import { Clipboard, Check } from 'lucide-react';
3
+
4
+ const InstallLine: React.FC<{ packageName: string }> = ({ packageName }) => {
5
+ const [hasCopied, setHasCopied] = useState(false);
6
+ const command = `npm i ${packageName}`;
7
+
8
+ const copyToClipboard = () => {
9
+ navigator.clipboard.writeText(command);
10
+ setHasCopied(true);
11
+ setTimeout(() => setHasCopied(false), 2000);
12
+ };
13
+
14
+ return (
15
+ <div className="flex items-center justify-center p-2">
16
+ <div
17
+ className="flex max-w-sm w-full items-center justify-between bg-white border border-gray-200 rounded-md py-1.5 px-2 font-mono text-xs shadow-sm hover:shadow transition-all duration-200 ease-in-out cursor-pointer"
18
+ onClick={copyToClipboard}
19
+ role="button"
20
+ tabIndex={0}
21
+ aria-label="Copy npm install command"
22
+ >
23
+ <div className="flex items-center font-bold space-x-2 overflow-x-auto">
24
+ <span className="text-gray-400 select-none">$</span>
25
+ <span className="text-blue-500 font-semibold">npm</span>
26
+ <span className="text-green-600">i</span>
27
+ <span className="text-pink-500">{packageName}</span>
28
+ </div>
29
+ <div className="p-1 flex-shrink-0" aria-hidden="true">
30
+ {hasCopied ? (
31
+ <Check className="size-4 text-emerald-500" />
32
+ ) : (
33
+ <Clipboard className="size-4 text-gray-400 hover:text-gray-600" />
34
+ )}
35
+ </div>
36
+ </div>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export default InstallLine;
components/Navbar.tsx CHANGED
@@ -5,19 +5,24 @@ import { FileText, Github } from "lucide-react"
5
  export default function Navbar() {
6
  return (
7
  <nav className="border-b">
8
- <div className="container flex justify-end items-center py-4">
9
- <Button variant="ghost" asChild className="p-2 sm:p-4 mr-2">
10
- <Link href="https://huggingface.co/docs/transformers.js/en/index" target="_blank" rel="noopener noreferrer" className="flex items-center">
11
- <FileText className="h-4 w-4 mr-2" />
12
- <span className="font-medium">Docs</span>
13
- </Link>
14
- </Button>
15
- <Button variant="ghost" asChild className="p-2 sm:p-4">
16
- <Link href="https://github.com/cfahlgren1/transformersjs-playground" target="_blank" rel="noopener noreferrer" className="flex items-center">
17
- <Github className="h-4 w-4 mr-2" />
18
- <span className="font-medium hidden sm:inline">Star on GitHub</span>
19
- </Link>
20
- </Button>
 
 
 
 
 
21
  </div>
22
  </nav>
23
  )
 
5
  export default function Navbar() {
6
  return (
7
  <nav className="border-b">
8
+ <div className="container mx-auto flex items-center justify-between py-4 px-4 sm:px-8">
9
+ <Link href="/" className="text-lg font-bold">
10
+ Playground
11
+ </Link>
12
+ <div className="flex items-center">
13
+ <Button variant="ghost" asChild className="p-2 sm:p-4 mr-2">
14
+ <Link href="https://huggingface.co/docs/transformers.js/en/index" target="_blank" rel="noopener noreferrer" className="flex items-center">
15
+ <FileText className="h-4 w-4 mr-2" />
16
+ <span className="font-medium">Docs</span>
17
+ </Link>
18
+ </Button>
19
+ <Button variant="ghost" asChild className="p-2 sm:p-4">
20
+ <Link href="https://github.com/cfahlgren1/transformersjs-playground" target="_blank" rel="noopener noreferrer" className="flex items-center">
21
+ <Github className="h-4 w-4 mr-2" />
22
+ <span className="font-medium hidden sm:inline">Star on GitHub</span>
23
+ </Link>
24
+ </Button>
25
+ </div>
26
  </div>
27
  </nav>
28
  )
components/PillLink.tsx CHANGED
@@ -1,5 +1,4 @@
1
  import Link from 'next/link';
2
- import { Badge } from '@/components/ui/badge';
3
 
4
  interface PillLinkProps {
5
  text: string;
@@ -8,32 +7,21 @@ interface PillLinkProps {
8
  newText?: string;
9
  }
10
 
11
- const PillLink: React.FC<PillLinkProps> = ({ text, link, isNew = false, newText = 'NEW' }) => {
12
- return (
13
- <Link target="_blank" href={link}>
14
- <Badge
15
- className="
16
- mb-4 bg-gray-900 px-3 py-1.5 text-xs font-medium text-white
17
- shadow-md transition-all duration-200
18
- hover:bg-gray-800 hover:shadow-lg
19
- transform hover:scale-105
20
- rounded-full relative
21
- overflow-hidden
22
- group
23
- "
24
- >
25
- <span className="relative z-10">
26
- {(isNew || newText) && (
27
- <span className="mr-2 rounded-full bg-white px-1.5 py-0.5 text-[10px] font-bold text-gray-900">
28
- {newText}
29
- </span>
30
- )}
31
- {text}
32
  </span>
33
- <span className="absolute inset-0 bg-gradient-to-r from-white to-gray-300 opacity-0 group-hover:opacity-20 transition-opacity duration-200"></span>
34
- </Badge>
35
- </Link>
36
- );
37
- };
38
 
39
  export default PillLink;
 
1
  import Link from 'next/link';
 
2
 
3
  interface PillLinkProps {
4
  text: string;
 
7
  newText?: string;
8
  }
9
 
10
+ const PillLink: React.FC<PillLinkProps> = ({ text, link, isNew = false, newText = 'NEW' }) => (
11
+ <Link
12
+ href={link}
13
+ target="_blank"
14
+ className="inline-flex items-center bg-white border border-gray-200 rounded-full py-1 px-2.5 mb-3 shadow-sm hover:shadow transition-all duration-200 ease-in-out cursor-pointer"
15
+ >
16
+ <span className="font-mono text-xs flex items-center">
17
+ {(isNew || newText) && (
18
+ <span className="mr-1.5 rounded-full bg-red-500 px-2 py-0.5 text-[8px] font-bold text-white">
19
+ {newText}
 
 
 
 
 
 
 
 
 
 
 
20
  </span>
21
+ )}
22
+ <span className="text-gray-800 text-[11px]">{text}</span>
23
+ </span>
24
+ </Link>
25
+ );
26
 
27
  export default PillLink;
components/{CategoryCard.tsx β†’ category/CategoryCard.tsx} RENAMED
@@ -1,7 +1,7 @@
1
  import React from 'react';
2
  import Link from 'next/link';
3
- import type { Category } from '../types/categories';
4
- import { getColorConfig } from '../lib/utils';
5
 
6
  type CategoryCardProps = Category;
7
 
 
1
  import React from 'react';
2
  import Link from 'next/link';
3
+ import type { Category } from '../../types/categories';
4
+ import { getColorConfig } from '../../lib/utils';
5
 
6
  type CategoryCardProps = Category;
7
 
components/category/CategoryContent.tsx ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { notFound } from 'next/navigation';
6
+ import { categories } from '@/lib/categories';
7
+ import type { Category } from '@/types/categories';
8
+ import { getColorConfig, cn } from '@/lib/utils';
9
+
10
+ export default function CategoryContent({ slug }: { slug: string }) {
11
+ const [category, setCategory] = useState<Category | null>(null);
12
+ const [isLoading, setIsLoading] = useState(true);
13
+
14
+ useEffect(() => {
15
+ const foundCategory = categories.find((cat) => cat.slug === slug);
16
+ setCategory(foundCategory || null);
17
+ setIsLoading(false);
18
+ }, [slug]);
19
+
20
+ if (isLoading) {
21
+ return <div className="flex justify-center items-center h-screen">Loading...</div>;
22
+ }
23
+
24
+ if (!category) {
25
+ notFound();
26
+ }
27
+
28
+ const colorConfig = getColorConfig(category.colorName);
29
+
30
+ return (
31
+ <div className="container mx-auto px-4 py-8">
32
+ <Breadcrumb category={category} colorConfig={colorConfig} />
33
+ <Header category={category} colorConfig={colorConfig} />
34
+ <div className="mt-12">
35
+ <DemoCards category={category} colorConfig={colorConfig} />
36
+ </div>
37
+ </div>
38
+ );
39
+ }
40
+
41
+
42
+ function Breadcrumb({ category, colorConfig }: { category: Category; colorConfig: ReturnType<typeof getColorConfig> }) {
43
+ return (
44
+ <nav className="text-sm mb-4">
45
+ <ol className="list-none p-0 inline-flex">
46
+ <li className="flex items-center">
47
+ <Link href="/" className="text-slate-600 hover:underline">
48
+ Home
49
+ </Link>
50
+ <span className="mx-2 text-slate-400">/</span>
51
+ </li>
52
+ <li className={cn("text-opacity-80", colorConfig.text)}>{category.title}</li>
53
+ </ol>
54
+ </nav>
55
+ );
56
+ }
57
+
58
+ function Header({ category, colorConfig }: { category: Category; colorConfig: ReturnType<typeof getColorConfig> }) {
59
+ return (
60
+ <header className="text-center">
61
+ <h1 className={cn("text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tighter mb-4", colorConfig.text)}>
62
+ {category.title}
63
+ </h1>
64
+ <p className="font-bold text-slate-700 font-mono italic text-lg mb-10">
65
+ {category.description}
66
+ </p>
67
+ </header>
68
+ );
69
+ }
70
+
71
+ function DemoCards({ category, colorConfig }: { category: Category; colorConfig: ReturnType<typeof getColorConfig> }) {
72
+ if (!category.demos || category.demos.length === 0) {
73
+ return (
74
+ <div className="flex flex-col items-center justify-center h-64 md:h-96 px-4 py-8 bg-gray-50 rounded-lg shadow-inner">
75
+ <div className={cn("text-6xl mb-4", colorConfig.text)}>
76
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black" className="w-16 h-16">
77
+ <path fillRule="evenodd" d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-2.625 6c-.54 0-.828.419-.936.634a1.96 1.96 0 00-.189.866c0 .298.059.605.189.866.108.215.395.634.936.634.54 0 .828-.419.936-.634.13-.26.189-.568.189-.866 0-.298-.059-.605-.189-.866-.108-.215-.395-.634-.936-.634zm4.314.634c.108-.215.395-.634.936-.634.54 0 .828.419.936.634.13.26.189.568.189.866 0 .298-.059.605-.189.866-.108.215-.395.634-.936.634-.54 0-.828-.419-.936-.634a1.96 1.96 0 01-.189-.866c0-.298.059-.605.189-.866zm-4.34 7.964a.75.75 0 01-1.061-1.06 5.236 5.236 0 013.73-1.538 5.236 5.236 0 013.695 1.538.75.75 0 11-1.061 1.06 3.736 3.736 0 00-2.639-1.098 3.736 3.736 0 00-2.664 1.098z" clipRule="evenodd" />
78
+ </svg>
79
+ </div>
80
+ <h2 className={cn("text-2xl md:text-3xl font-bold text-center mb-2")}>
81
+ No demos available yet
82
+ </h2>
83
+ <p className="text-base md:text-lg text-gray-600 text-center max-w-md">
84
+ We&apos;re working on exciting demos for this category. Check back soon to see what&apos;s new!
85
+ </p>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ return (
91
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
92
+ {category.demos.map((Demo, index) => (
93
+ <div key={index} className={cn("rounded-lg shadow-md p-6", colorConfig.bg)}>
94
+ <Demo />
95
+ </div>
96
+ ))}
97
+ </div>
98
+ );
99
+ }
components/{CategoryPage.tsx β†’ category/CategoryPage.tsx} RENAMED
File without changes
components/{CategoryRow.tsx β†’ category/CategoryRow.tsx} RENAMED
@@ -1,5 +1,5 @@
1
  import { motion } from 'framer-motion';
2
- import CategoryCard from '@/components/CategoryCard';
3
  import { Category } from '@/types/categories';
4
 
5
  interface CategoryRowProps {
 
1
  import { motion } from 'framer-motion';
2
+ import CategoryCard from '@/components/category/CategoryCard';
3
  import { Category } from '@/types/categories';
4
 
5
  interface CategoryRowProps {
lib/icons/ClassificationIcon.tsx CHANGED
@@ -23,17 +23,20 @@ const ClassificationIcon: React.FC<ClassificationIconProps> = ({ className = ''
23
  <text x="130" y="25" fill="#7e22ce" fontSize="12" fontWeight="bold">Output</text>
24
 
25
  {/* Classification bars */}
 
26
  <rect x="130" y="35" width="65" height="14" fill="#d8b4fe" rx="3" ry="3" />
27
  <text x="135" y="45" fill="#6b21a8" fontSize="10" fontWeight="bold">Sport</text>
28
- <text x="188" y="45" fill="#6b21a8" fontSize="10" textAnchor="end">0.800</text>
29
 
 
30
  <rect x="130" y="53" width="26" height="14" fill="#d8b4fe" rx="3" ry="3" />
31
  <text x="135" y="63" fill="#6b21a8" fontSize="10" fontWeight="bold">Food</text>
32
- <text x="188" y="63" fill="#6b21a8" fontSize="10" textAnchor="end">0.150</text>
33
 
 
34
  <rect x="130" y="71" width="9" height="14" fill="#d8b4fe" rx="3" ry="3" />
35
  <text x="135" y="81" fill="#6b21a8" fontSize="10" fontWeight="bold">Music</text>
36
- <text x="188" y="81" fill="#6b21a8" fontSize="10" textAnchor="end">0.050</text>
37
  </svg>
38
  );
39
  };
 
23
  <text x="130" y="25" fill="#7e22ce" fontSize="12" fontWeight="bold">Output</text>
24
 
25
  {/* Classification bars */}
26
+ {/* Sport */}
27
  <rect x="130" y="35" width="65" height="14" fill="#d8b4fe" rx="3" ry="3" />
28
  <text x="135" y="45" fill="#6b21a8" fontSize="10" fontWeight="bold">Sport</text>
29
+ <text x="200" y="45" fill="#6b21a8" fontSize="10" textAnchor="end">0.800</text>
30
 
31
+ {/* Food */}
32
  <rect x="130" y="53" width="26" height="14" fill="#d8b4fe" rx="3" ry="3" />
33
  <text x="135" y="63" fill="#6b21a8" fontSize="10" fontWeight="bold">Food</text>
34
+ <text x="200" y="63" fill="#6b21a8" fontSize="10" textAnchor="end">0.150</text>
35
 
36
+ {/* Music */}
37
  <rect x="130" y="71" width="9" height="14" fill="#d8b4fe" rx="3" ry="3" />
38
  <text x="135" y="81" fill="#6b21a8" fontSize="10" fontWeight="bold">Music</text>
39
+ <text x="200" y="81" fill="#6b21a8" fontSize="10" textAnchor="end">0.050</text>
40
  </svg>
41
  );
42
  };
next.config.mjs CHANGED
@@ -1,7 +1,7 @@
1
  /** @type {import('next').NextConfig} */
2
  const nextConfig = {
3
- output: 'export',
4
- distDir: 'dist',
5
  };
6
 
7
  export default nextConfig;
 
1
  /** @type {import('next').NextConfig} */
2
  const nextConfig = {
3
+ output: "standalone",
4
+ reactStrictMode: true,
5
  };
6
 
7
  export default nextConfig;
types/categories.ts CHANGED
@@ -10,4 +10,5 @@ export interface Category {
10
  status: CategoryStatus;
11
  colorName: string;
12
  graphic: React.ComponentType<React.SVGProps<SVGSVGElement>>;
 
13
  }
 
10
  status: CategoryStatus;
11
  colorName: string;
12
  graphic: React.ComponentType<React.SVGProps<SVGSVGElement>>;
13
+ demos?: React.ComponentType[];
14
  }