├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── README.md ├── apps ├── docs │ ├── .eslintrc.js │ ├── README.md │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ └── page.tsx │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public │ │ ├── circles.svg │ │ ├── next.svg │ │ ├── turborepo.svg │ │ └── vercel.svg │ └── tsconfig.json └── web │ ├── .env.example │ ├── .eslintrc.js │ ├── .gitignore │ ├── README.md │ ├── app │ ├── (providers) │ │ ├── shadcn │ │ │ ├── components │ │ │ │ ├── blocks │ │ │ │ │ ├── charts-01.tsx │ │ │ │ │ ├── dashboard-05.tsx │ │ │ │ │ ├── dashboard-06.tsx │ │ │ │ │ └── dashboard-07.tsx │ │ │ │ ├── footer.tsx │ │ │ │ ├── responsive-theme-editor.tsx │ │ │ │ ├── theme-export.tsx │ │ │ │ ├── theme-generator-input.tsx │ │ │ │ ├── theme-generator-properties.tsx │ │ │ │ ├── theme-generator.tsx │ │ │ │ ├── theme-install-code.tsx │ │ │ │ ├── theme-preset-selector.tsx │ │ │ │ ├── theme-preview.tsx │ │ │ │ └── theme-workspace.tsx │ │ │ ├── constants.ts │ │ │ └── page.tsx │ │ └── vscode │ │ │ ├── editor │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── api │ │ ├── enhance │ │ │ ├── route.ts │ │ │ ├── shadcn │ │ │ │ └── route.ts │ │ │ └── vscode │ │ │ │ └── route.ts │ │ ├── generate │ │ │ ├── shadcn │ │ │ │ └── route.ts │ │ │ └── vscode │ │ │ │ └── route.ts │ │ ├── inngest │ │ │ └── route.ts │ │ ├── og │ │ │ └── route.tsx │ │ ├── shadcn │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── subscribe │ │ │ └── route.ts │ │ ├── theme │ │ │ ├── [id] │ │ │ │ ├── name │ │ │ │ │ └── route.ts │ │ │ │ ├── route.ts │ │ │ │ └── status │ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── themes │ │ │ └── route.ts │ │ └── unsubscribe │ │ │ └── route.ts │ ├── globals.css │ ├── icon.svg │ ├── layout.tsx │ ├── page.tsx │ ├── s │ │ └── [id] │ │ │ └── route.ts │ ├── t │ │ └── [id] │ │ │ ├── page.tsx │ │ │ └── theme-content.tsx │ └── utils.ts │ ├── components.json │ ├── components │ ├── achievement-banner.tsx │ ├── atom │ │ ├── rh-tinte-link-logo.tsx │ │ └── tinte-link-logo.tsx │ ├── browser-theme-selector.tsx │ ├── circular-gradient.tsx │ ├── color-palette.tsx │ ├── color-picker-button.tsx │ ├── color-picker-input.tsx │ ├── counterscale-script.tsx │ ├── create-theme-dialog.tsx │ ├── delete-theme-dialog.tsx │ ├── dynamic-accent-title.tsx │ ├── empty-state.tsx │ ├── general-header.tsx │ ├── header.tsx │ ├── help-dialog.tsx │ ├── landing-theme-generator.tsx │ ├── language-switcher.tsx │ ├── load-more-skeleton.tsx │ ├── loading-page.tsx │ ├── logo-3d.tsx │ ├── preview-editor.tsx │ ├── providers.tsx │ ├── read-only-preview.tsx │ ├── readonly-preview-editor.tsx │ ├── scroll-to-top.tsx │ ├── share-theme-dialog.tsx │ ├── sign-in-dialog.tsx │ ├── simplified-token-editor.tsx │ ├── subscribed-email.tsx │ ├── subscription-form.tsx │ ├── theme-card-options.tsx │ ├── theme-card.tsx │ ├── theme-cards.tsx │ ├── theme-config-panel.tsx │ ├── theme-control-bar.tsx │ ├── theme-customizer.tsx │ ├── theme-generator.tsx │ ├── theme-manager.tsx │ ├── theme-preview.tsx │ ├── theme-selector.tsx │ ├── theme-sheet.tsx │ ├── tinte-for-shadcn-modal.tsx │ ├── token-editor.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── breadcrumb.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── icons.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── resizable.tsx │ │ ├── responsive-modal.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── shine-button.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx │ ├── inngest │ ├── client.ts │ ├── functions.ts │ └── sync-user.ts │ ├── lib │ ├── actions.ts │ ├── actions │ │ └── shadcn-theme-actions.ts │ ├── api.ts │ ├── atoms │ │ └── index.ts │ ├── constants.tsx │ ├── copy-code │ │ └── generators.ts │ ├── core │ │ ├── colors.ts │ │ ├── config.ts │ │ ├── index.ts │ │ ├── tokens.ts │ │ └── types.ts │ ├── export-theme.ts │ ├── hooks │ │ ├── use-binary-theme.ts │ │ ├── use-code-sample.ts │ │ ├── use-debounce.ts │ │ ├── use-highlighter.tsx │ │ ├── use-infinite-themes.ts │ │ ├── use-media-query.ts │ │ ├── use-monaco-editor.tsx │ │ ├── use-shadcn-theme-generator.ts │ │ ├── use-theme-applier.ts │ │ ├── use-theme-config.ts │ │ ├── use-theme-enhancer.ts │ │ ├── use-theme-export.ts │ │ └── use-theme-generator.ts │ ├── language-logos.tsx │ ├── themes │ │ ├── one-hunter-theme-dark.json │ │ └── one-hunter-theme-light.json │ ├── types.ts │ └── utils.ts │ ├── middleware.ts │ ├── netlify.toml │ ├── netlify │ └── functions │ │ └── createVSIX.js │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ └── api │ │ └── export-vsix.ts │ ├── postcss.config.js │ ├── prisma │ └── schema.prisma │ ├── public │ ├── circles.svg │ ├── flexoki-logo.png │ ├── fonts │ │ ├── Geist-Bold.ttf │ │ ├── Geist-Regular.ttf │ │ └── GeistMono-Regular.ttf │ ├── logos │ │ ├── angular.svg │ │ ├── astro.svg │ │ ├── bash.svg │ │ ├── c.svg │ │ ├── cpp.svg │ │ ├── csharp.svg │ │ ├── css.svg │ │ ├── dart.svg │ │ ├── go.svg │ │ ├── html.svg │ │ ├── java.svg │ │ ├── javascript.svg │ │ ├── json.svg │ │ ├── jsx.svg │ │ ├── kotlin.svg │ │ ├── lua.svg │ │ ├── markdown.svg │ │ ├── ocaml.svg │ │ ├── php.svg │ │ ├── python.svg │ │ ├── r.svg │ │ ├── raycast.svg │ │ ├── ruby.svg │ │ ├── rust.svg │ │ ├── scala.svg │ │ ├── solidity.svg │ │ ├── sql.svg │ │ ├── svelte.svg │ │ ├── swift.svg │ │ ├── toml.svg │ │ ├── tsx.svg │ │ ├── typescript.svg │ │ ├── vue.svg │ │ └── zig.svg │ ├── next.svg │ ├── og-image.png │ ├── one-hunter-logo.png │ ├── placeholder.svg │ ├── rh-logo.svg │ ├── supabase-icon 1.svg │ ├── tailwindcss-icon 1.svg │ ├── themes │ │ └── zaffre.json │ ├── tinte-1.png │ ├── tinte-2.png │ ├── tinte-3.png │ ├── turborepo.svg │ ├── vercel-icon 1.png │ ├── vercel-icon 1.svg │ └── vercel.svg │ ├── tailwind.config.js │ ├── tsconfig.json │ └── types.d.ts ├── package.json ├── packages ├── eslint-config │ ├── README.md │ ├── library.js │ ├── next.js │ ├── package.json │ └── react-internal.js ├── tinte-core │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── flexoki-dark.jpg │ │ ├── flexoki-light.jpg │ │ ├── one-hunter-dark.jpg │ │ ├── one-hunter-light.jpg │ │ ├── tinte-logo.png │ │ └── transparent.png │ ├── src │ │ ├── config │ │ │ ├── customize │ │ │ │ └── vscode.ts │ │ │ └── index.ts │ │ ├── generators │ │ │ ├── alacritty │ │ │ │ └── generate.ts │ │ │ ├── fzf │ │ │ │ └── generate.ts │ │ │ ├── gimp │ │ │ │ ├── generate.ts │ │ │ │ └── mappers.ts │ │ │ ├── index.ts │ │ │ ├── iterm2 │ │ │ │ ├── generate.ts │ │ │ │ └── mappers.ts │ │ │ ├── kitty │ │ │ │ └── generate.ts │ │ │ ├── lite-xl │ │ │ │ └── generate.ts │ │ │ ├── theme-sh │ │ │ │ └── generate.ts │ │ │ ├── types.ts │ │ │ ├── vanilla-css │ │ │ │ ├── generate.ts │ │ │ │ └── mappers.ts │ │ │ ├── vscode │ │ │ │ ├── generate.ts │ │ │ │ └── mappers.ts │ │ │ ├── warp │ │ │ │ └── generate.ts │ │ │ ├── wezterm │ │ │ │ └── generate.ts │ │ │ ├── windows-terminal │ │ │ │ └── generate.ts │ │ │ └── xresources │ │ │ │ └── generate.ts │ │ ├── index.ts │ │ ├── mapped-palette.ts │ │ ├── palettes │ │ │ ├── flexoki.ts │ │ │ ├── one-hunter-material.ts │ │ │ ├── types.ts │ │ │ └── vercel-2024.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── color.ts │ │ │ ├── format.ts │ │ │ └── index.ts │ └── tsconfig.json ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── .eslintrc.js │ ├── package.json │ ├── src │ ├── button.tsx │ ├── card.tsx │ └── code.tsx │ ├── tsconfig.json │ ├── tsconfig.lint.json │ └── turbo │ └── generators │ ├── config.ts │ └── templates │ └── component.hbs ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tinte-preview-2.png ├── tinte-preview.png ├── tsconfig.json └── turbo.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This configuration only applies to the package manager root. 2 | /** @type {import("eslint").Linter.Config} */ 3 | module.exports = { 4 | ignorePatterns: ["apps/**", "packages/**"], 5 | extends: ["@repo/eslint-config/library.js"], 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | project: true, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | 40 | # Local Netlify folder 41 | .netlify -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/.npmrc -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/docs/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/docs/README.md: -------------------------------------------------------------------------------- 1 | ## Getting Started 2 | 3 | First, run the development server: 4 | 5 | ```bash 6 | yarn dev 7 | ``` 8 | 9 | Open [http://localhost:3001](http://localhost:3001) with your browser to see the result. 10 | 11 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 12 | 13 | To create [API routes](https://nextjs.org/docs/app/building-your-application/routing/router-handlers) add an `api/` directory to the `app/` directory with a `route.ts` file. For individual endpoints, create a subfolder in the `api` directory, like `api/hello/route.ts` would map to [http://localhost:3001/api/hello](http://localhost:3001/api/hello). 14 | 15 | ## Learn More 16 | 17 | To learn more about Next.js, take a look at the following resources: 18 | 19 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 20 | - [Learn Next.js](https://nextjs.org/learn/foundations/about-nextjs) - an interactive Next.js tutorial. 21 | 22 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 23 | 24 | ## Deploy on Vercel 25 | 26 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js. 27 | 28 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 29 | -------------------------------------------------------------------------------- /apps/docs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Railly/tinte/c8bfdff75d5620bde69200f5f71d0f6082130b85/apps/docs/app/favicon.ico -------------------------------------------------------------------------------- /apps/docs/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", 5 | "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", 6 | "Fira Mono", "Droid Sans Mono", "Courier New", monospace; 7 | 8 | --foreground-rgb: 255, 255, 255; 9 | --background-start-rgb: 0, 0, 0; 10 | --background-end-rgb: 0, 0, 0; 11 | 12 | --callout-rgb: 20, 20, 20; 13 | --callout-border-rgb: 108, 108, 108; 14 | --card-rgb: 100, 100, 100; 15 | --card-border-rgb: 200, 200, 200; 16 | 17 | --glow-conic: conic-gradient( 18 | from 180deg at 50% 50%, 19 | #2a8af6 0deg, 20 | #a853ba 180deg, 21 | #e92a67 360deg 22 | ); 23 | } 24 | 25 | * { 26 | box-sizing: border-box; 27 | padding: 0; 28 | margin: 0; 29 | } 30 | 31 | html, 32 | body { 33 | max-width: 100vw; 34 | overflow-x: hidden; 35 | } 36 | 37 | body { 38 | color: rgb(var(--foreground-rgb)); 39 | background: linear-gradient( 40 | to bottom, 41 | transparent, 42 | rgb(var(--background-end-rgb)) 43 | ) 44 | rgb(var(--background-start-rgb)); 45 | } 46 | 47 | a { 48 | color: inherit; 49 | text-decoration: none; 50 | } 51 | -------------------------------------------------------------------------------- /apps/docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Turborepo", 9 | description: "Generated by create turbo", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: { 15 | children: React.ReactNode; 16 | }): JSX.Element { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /apps/docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /apps/docs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | transpilePackages: ["@repo/ui"], 4 | }; 5 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --port 3001", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint . --max-warnings 0" 10 | }, 11 | "dependencies": { 12 | "@repo/ui": "workspace:*", 13 | "next": "^14.1.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@next/eslint-plugin-next": "^14.1.1", 19 | "@repo/eslint-config": "workspace:*", 20 | "@repo/typescript-config": "workspace:*", 21 | "@types/eslint": "^8.56.5", 22 | "@types/node": "^20.11.24", 23 | "@types/react": "^18.2.61", 24 | "@types/react-dom": "^18.2.19", 25 | "eslint": "^8.57.0", 26 | "typescript": "^5.3.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apps/docs/public/circles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /apps/docs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/docs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [ 5 | { 6 | "name": "next" 7 | } 8 | ] 9 | }, 10 | "include": [ 11 | "next-env.d.ts", 12 | "next.config.js", 13 | "**/*.ts", 14 | "**/*.tsx", 15 | ".next/types/**/*.ts" 16 | ], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_EXPORT_API_URL= 2 | OPENAI_API_KEY= 3 | UPSTASH_REDIS_REST_URL= 4 | UPSTASH_REDIS_REST_TOKEN= 5 | RESEND_API_KEY= 6 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 7 | CLERK_SECRET_KEY= -------------------------------------------------------------------------------- /apps/web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** @type {import("eslint").Linter.Config} */ 2 | module.exports = { 3 | root: true, 4 | extends: ["@repo/eslint-config/next.js"], 5 | parser: "@typescript-eslint/parser", 6 | parserOptions: { 7 | project: true, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /apps/web/app/(providers)/shadcn/components/responsive-theme-editor.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | Popover, 5 | PopoverContent, 6 | PopoverTrigger, 7 | } from "@/components/ui/popover"; 8 | import { Drawer, DrawerContent, DrawerTrigger } from "@/components/ui/drawer"; 9 | import { IconPalette } from "@/components/ui/icons"; 10 | import { ThemeGeneratorProperties } from "./theme-generator-properties"; 11 | import { Theme } from "@/lib/atoms"; 12 | import { useMediaQuery } from "@/lib/hooks/use-media-query"; 13 | 14 | interface ResponsiveThemeEditorProps { 15 | currentTheme: Theme; 16 | setCurrentTheme: React.Dispatch>; 17 | copyCode: (format: "css" | "tailwind" | "json") => void; 18 | } 19 | 20 | export function ResponsiveThemeEditor({ 21 | currentTheme, 22 | setCurrentTheme, 23 | copyCode, 24 | }: ResponsiveThemeEditorProps) { 25 | const [open, setOpen] = React.useState(false); 26 | const isDesktop = useMediaQuery("(min-width: 768px)"); 27 | 28 | const content = ( 29 | 34 | ); 35 | 36 | if (isDesktop) { 37 | return ( 38 | 39 | 40 | 44 | 45 | 46 | {content} 47 | 48 | 49 | ); 50 | } 51 | 52 | return ( 53 | 54 | 55 | 59 | 60 | 61 |
{content}
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /apps/web/app/(providers)/shadcn/components/theme-export.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { motion } from "framer-motion"; 5 | import { useAtomValue } from "jotai"; 6 | import { themeAtom } from "@/lib/atoms"; 7 | import { Button } from "@/components/ui/button"; 8 | import { IconDownload, IconShare } from "@/components/ui/icons"; 9 | import { 10 | DropdownMenu, 11 | DropdownMenuContent, 12 | DropdownMenuItem, 13 | DropdownMenuTrigger, 14 | } from "@/components/ui/dropdown-menu"; 15 | 16 | export function ThemeExport() { 17 | const theme = useAtomValue(themeAtom); 18 | 19 | const exportTheme = () => { 20 | const themeString = JSON.stringify(theme, null, 2); 21 | const blob = new Blob([themeString], { type: "application/json" }); 22 | const url = URL.createObjectURL(blob); 23 | const a = document.createElement("a"); 24 | a.href = url; 25 | a.download = "tinte-theme.json"; 26 | a.click(); 27 | URL.revokeObjectURL(url); 28 | }; 29 | 30 | return ( 31 |
32 | 33 | 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | console.log("Copy link")}> 48 | Copy Link 49 | 50 | console.log("Share on Twitter")}> 51 | Share on Twitter 52 | 53 | 54 | 55 | 56 |
57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /apps/web/app/(providers)/shadcn/components/theme-workspace.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { DynamicAccentTitle } from "@/components/dynamic-accent-title"; 5 | import { IconPalette } from "@/components/ui/icons"; 6 | import { createCopyCodeFunction } from "@/lib/copy-code/generators"; 7 | import { GeneralHeader } from "@/components/general-header"; 8 | import { useThemeApplier } from "@/lib/hooks/use-theme-applier"; 9 | import { ResponsiveThemeEditor } from "./responsive-theme-editor"; 10 | import { ThemeGenerator } from "./theme-generator"; 11 | import { ThemePreview } from "./theme-preview"; 12 | import { ShadcnThemes } from "@prisma/client"; 13 | import { ThemeInstallCode } from "./theme-install-code"; 14 | 15 | interface ThemeWorkspaceProps { 16 | allThemes: ShadcnThemes[]; 17 | initialPagination: { 18 | currentPage: number; 19 | totalPages: number; 20 | totalItems: number; 21 | }; 22 | } 23 | 24 | export function ThemeWorkspace({ 25 | allThemes, 26 | initialPagination, 27 | }: ThemeWorkspaceProps) { 28 | const { currentTheme, currentChartTheme, setCurrentTheme } = 29 | useThemeApplier(); 30 | 31 | const copyCode = createCopyCodeFunction(currentTheme); 32 | 33 | const headerActions = [ 34 | { 35 | label: "Customize", 36 | icon: IconPalette, 37 | component: ( 38 | 43 | ), 44 | }, 45 | ]; 46 | 47 | return ( 48 | <> 49 | 50 |
51 | 59 | 65 | 66 |
67 |
68 | 73 |
74 |
75 |
76 | 77 | ); 78 | } 79 | -------------------------------------------------------------------------------- /apps/web/app/(providers)/shadcn/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Footer } from "./components/footer"; 3 | import { getThemes } from "@/lib/actions/shadcn-theme-actions"; 4 | import { ThemeWorkspace } from "./components/theme-workspace"; 5 | import { cookies } from "next/headers"; 6 | import { TinteForShadcnModal } from "@/components/tinte-for-shadcn-modal"; 7 | 8 | export default async function ShadcnThemesPage() { 9 | const { themes, pagination } = await getThemes(1, 5); 10 | const cookieStore = cookies(); 11 | const hasSeenModal = cookieStore.get("hasSeenTinteForShadcnModal"); 12 | 13 | return ( 14 |
15 | 16 |
17 | {!hasSeenModal && } 18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/(providers)/vscode/editor/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ThemeCustomizer } from "@/components/theme-customizer"; 3 | import { getAllThemes } from "@/lib/api"; 4 | 5 | export default async function VSCodePage() { 6 | const allThemes = await getAllThemes(); 7 | 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/app/(providers)/vscode/page.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cookies } from "next/headers"; 3 | import { ThemeManager } from "@/components/theme-manager"; 4 | import { GeneralHeader } from "@/components/general-header"; 5 | import { TinteForShadcnModal } from "@/components/tinte-for-shadcn-modal"; 6 | import { getInitialThemes } from "@/lib/api"; 7 | 8 | export default async function Page() { 9 | const initialThemes = await getInitialThemes(); 10 | const cookieStore = cookies(); 11 | const hasSeenModal = cookieStore.get("hasSeenTinteForShadcnModal"); 12 | 13 | return ( 14 | <> 15 | 16 | 17 | {!hasSeenModal && } 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/api/enhance/route.ts: -------------------------------------------------------------------------------- 1 | import { generateObject } from "ai"; 2 | import { openai } from "@ai-sdk/openai"; 3 | import { z } from "zod"; 4 | import { Ratelimit } from "@upstash/ratelimit"; 5 | import { Redis } from "@upstash/redis"; 6 | import { NextRequest } from "next/server"; 7 | 8 | export const maxDuration = 15; 9 | 10 | const ratelimit = new Ratelimit({ 11 | redis: Redis.fromEnv(), 12 | limiter: Ratelimit.slidingWindow(20, "10s"), 13 | analytics: true, 14 | }); 15 | 16 | const outputSchema = z.object({ 17 | enhancedPrompt: z 18 | .string() 19 | .min(10) 20 | .max(200) 21 | .describe("Enhanced theme description prompt"), 22 | }); 23 | 24 | export async function POST(req: NextRequest) { 25 | const ip = req.ip ?? req.headers.get("X-Forwarded-For") ?? "ip"; 26 | const { success } = await ratelimit.limit(ip); 27 | const { prompt }: { prompt: string } = await req.json(); 28 | 29 | if (!success) { 30 | return new Response("Ratelimited!", { status: 429 }); 31 | } 32 | 33 | const result = await generateObject({ 34 | model: openai("gpt-4o-mini"), 35 | system: `You are an expert at enhancing theme descriptions for Visual Studio Code. Your task is to take a user's initial theme idea and expand it into a more detailed and creative description. Follow these guidelines: 36 | - Maintain the core concept of the original prompt 37 | - Add specific color suggestions or palettes 38 | - Keep the enhanced prompt concise (max 150 characters) 39 | - Use descriptive and evocative language`, 40 | schema: outputSchema, 41 | prompt: `Enhance this theme description: "${prompt}"`, 42 | }); 43 | 44 | return Response.json({ enhancedPrompt: result.object.enhancedPrompt }); 45 | } 46 | -------------------------------------------------------------------------------- /apps/web/app/api/enhance/vscode/route.ts: -------------------------------------------------------------------------------- 1 | import { generateObject } from "ai"; 2 | import { openai } from "@ai-sdk/openai"; 3 | import { z } from "zod"; 4 | import { Ratelimit } from "@upstash/ratelimit"; 5 | import { Redis } from "@upstash/redis"; 6 | import { NextRequest } from "next/server"; 7 | import { track } from "@vercel/analytics/server"; 8 | 9 | export const maxDuration = 15; 10 | 11 | const ratelimit = new Ratelimit({ 12 | redis: Redis.fromEnv(), 13 | limiter: Ratelimit.slidingWindow(20, "10s"), 14 | analytics: true, 15 | }); 16 | 17 | const outputSchema = z.object({ 18 | enhancedPrompt: z 19 | .string() 20 | .describe( 21 | "Enhanced theme description prompt for vscode (ideally 10-200 characters)", 22 | ), 23 | }); 24 | 25 | export async function POST(req: NextRequest) { 26 | const ip = req.ip ?? req.headers.get("X-Forwarded-For") ?? "ip"; 27 | const { success } = await ratelimit.limit(ip); 28 | const { prompt }: { prompt: string } = await req.json(); 29 | 30 | if (!success) { 31 | await track("VSCode Theme Description Enhancement Ratelimited", { ip }); 32 | return new Response("Ratelimited!", { status: 429 }); 33 | } 34 | 35 | try { 36 | const result = await generateObject({ 37 | model: openai("gpt-4o-mini"), 38 | system: `You are an expert at enhancing theme descriptions for Visual Studio Code. Your task is to take a user's initial theme idea and expand it into a more detailed and creative description. Follow these guidelines: 39 | - Maintain the core concept of the original prompt 40 | - Add specific color suggestions or palettes 41 | - Keep the enhanced prompt concise (max 150 characters) 42 | - Use descriptive and evocative language`, 43 | schema: outputSchema, 44 | prompt: `Enhance this theme description: "${prompt}"`, 45 | }); 46 | 47 | await track("VSCode Theme Description Enhanced", { 48 | originalLength: prompt.length, 49 | enhancedLength: result.object.enhancedPrompt.length, 50 | }); 51 | 52 | return Response.json({ enhancedPrompt: result.object.enhancedPrompt }); 53 | } catch (error) { 54 | console.error("VSCode theme description enhancement error:", error); 55 | await track("VSCode Theme Description Enhancement Failed", { 56 | error: (error as Error).message, 57 | }); 58 | return Response.json( 59 | { error: "Failed to enhance VSCode theme description" }, 60 | { status: 500 }, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /apps/web/app/api/inngest/route.ts: -------------------------------------------------------------------------------- 1 | import { inngest } from "@/inngest/client"; 2 | import { functions } from "@/inngest/functions"; 3 | import { serve } from "inngest/next"; 4 | 5 | export const { GET, POST, PUT } = serve({ 6 | client: inngest, 7 | functions, 8 | }); 9 | -------------------------------------------------------------------------------- /apps/web/app/api/shadcn/route.ts: -------------------------------------------------------------------------------- 1 | // app/api/shadcn/route.ts 2 | 3 | import { NextRequest, NextResponse } from "next/server"; 4 | import { PrismaClient, ShadcnThemes } from "@prisma/client"; 5 | import { sanitizeJsonInput } from "@/lib/utils"; 6 | import { Theme } from "@/lib/atoms"; 7 | 8 | const prisma = new PrismaClient(); 9 | 10 | export async function POST(req: NextRequest) { 11 | const body = (await req.json()) as Partial & { userId: string | null }; 12 | 13 | try { 14 | const newTheme = await prisma.shadcnThemes.create({ 15 | data: { 16 | name: body.name!, 17 | display_name: body.displayName, 18 | User: body.userId, 19 | light_scheme: sanitizeJsonInput(body.light), 20 | dark_scheme: sanitizeJsonInput(body.dark), 21 | //fonts: sanitizeJsonInput(body.fonts), 22 | radius: String(body.radius), 23 | //space: String(body.space), 24 | //shadow: body.shadow, 25 | charts: sanitizeJsonInput(body.charts), 26 | //icons: body.icons, 27 | }, 28 | }); 29 | 30 | return NextResponse.json(newTheme); 31 | } catch (error) { 32 | console.error("Error creating ShadcnTheme:", error); 33 | return NextResponse.json( 34 | { error: "Failed to create theme" }, 35 | { status: 500 }, 36 | ); 37 | } finally { 38 | await prisma.$disconnect(); 39 | } 40 | } 41 | 42 | export async function GET(req: NextRequest) { 43 | const searchParams = req.nextUrl.searchParams; 44 | const page = Number(searchParams.get("page")) || 1; 45 | const limit = Number(searchParams.get("limit")) || 20; 46 | const userId = searchParams.get("userId"); 47 | 48 | try { 49 | const themes = await prisma.shadcnThemes.findMany({ 50 | where: userId ? { User: userId } : undefined, 51 | skip: (page - 1) * limit, 52 | take: limit, 53 | orderBy: { xata_createdat: "desc" }, 54 | }); 55 | 56 | const total = await prisma.shadcnThemes.count({ 57 | where: userId ? { User: userId } : undefined, 58 | }); 59 | 60 | return NextResponse.json({ 61 | themes: themes as ShadcnThemes[], 62 | pagination: { 63 | currentPage: page, 64 | totalPages: Math.ceil(total / limit), 65 | totalItems: total, 66 | }, 67 | }); 68 | } catch (error) { 69 | console.error("Error fetching ShadcnThemes:", error); 70 | return NextResponse.json( 71 | { error: "Failed to fetch themes" }, 72 | { status: 500 }, 73 | ); 74 | } finally { 75 | await prisma.$disconnect(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /apps/web/app/api/subscribe/route.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | import { Redis } from "@upstash/redis"; 3 | import { Ratelimit } from "@upstash/ratelimit"; 4 | import { NextRequest, NextResponse } from "next/server"; 5 | import { SubscribedEmail } from "@/components/subscribed-email"; 6 | 7 | const resend = new Resend(process.env.RESEND_API_KEY!); 8 | const redis = Redis.fromEnv(); 9 | const ratelimit = new Ratelimit({ 10 | redis: Redis.fromEnv(), 11 | limiter: Ratelimit.slidingWindow(10, "1 h"), 12 | }); 13 | 14 | export async function POST(request: NextRequest) { 15 | const { email, firstName } = await request.json(); 16 | 17 | const ip = request.ip ?? request.headers.get("X-Forwarded-For") ?? "ip"; 18 | 19 | const { success } = await ratelimit.limit(ip); 20 | if (!success) { 21 | return NextResponse.json( 22 | { error: "Too many requests. Please try again later." }, 23 | { status: 429 }, 24 | ); 25 | } 26 | 27 | const hashKey = `subscriptions:${email}`; 28 | const field = "tinte"; 29 | const productName = "Tinte"; 30 | 31 | const isSubscribed = await redis.hget(hashKey, field); 32 | 33 | if (isSubscribed) { 34 | return NextResponse.json( 35 | { error: "Email already subscribed" }, 36 | { status: 400 }, 37 | ); 38 | } 39 | 40 | await redis.hset(hashKey, { [field]: "subscribed" }); 41 | 42 | await resend.emails.send({ 43 | from: "Railly Hugo ", 44 | to: email, 45 | subject: "Subscription Confirmation", 46 | react: SubscribedEmail({ 47 | firstName, 48 | productName, 49 | unsubscribeLink: `https://tinte.railly.dev/api/unsubscribe?email=${encodeURIComponent( 50 | email, 51 | )}`, 52 | }), 53 | text: `Welcome, ${firstName}! Thank you for subscribing to ${productName}. We're thrilled to have you on board! As a subscriber, you'll be the first to know about our latest features, updates, and exclusive offers. If you wish to unsubscribe, click here: https://tinte.railly.dev/api/unsubscribe?email=${encodeURIComponent( 54 | email, 55 | )}`, 56 | }); 57 | 58 | return NextResponse.json( 59 | { message: "Email subscribed successfully" }, 60 | { status: 200 }, 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /apps/web/app/api/theme/[id]/name/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { revalidatePath } from "next/cache"; 3 | import { PrismaClient } from "@prisma/client"; 4 | import { getThemeName } from "@/app/utils"; 5 | 6 | const prisma = new PrismaClient(); 7 | 8 | export const PATCH = async ( 9 | req: Request, 10 | { params }: { params: { id: string } } 11 | ) => { 12 | const { id } = params; 13 | const { displayName, userId } = (await req.json()) as { 14 | displayName: string; 15 | userId: string; 16 | }; 17 | 18 | if (!userId) { 19 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 20 | } 21 | 22 | try { 23 | const theme = await prisma.themes.update({ 24 | where: { 25 | xata_id: id, 26 | User: userId, 27 | }, 28 | data: { 29 | name: getThemeName(displayName), 30 | display_name: displayName, 31 | }, 32 | }); 33 | 34 | revalidatePath("/", "page"); 35 | 36 | return NextResponse.json(theme); 37 | } catch (error) { 38 | console.error("Error updating theme name:", error); 39 | return NextResponse.json( 40 | { error: "Failed to update theme name" }, 41 | { status: 500 } 42 | ); 43 | } finally { 44 | await prisma.$disconnect(); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /apps/web/app/api/theme/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { revalidatePath } from "next/cache"; 3 | import { ThemeConfig } from "@/lib/core/types"; 4 | import { PrismaClient } from "@prisma/client"; 5 | import { invertPalette } from "@/app/utils"; 6 | 7 | const prisma = new PrismaClient(); 8 | 9 | export const PUT = async ( 10 | req: Request, 11 | { params }: { params: { id: string } } 12 | ) => { 13 | const { id } = params; 14 | const { name, displayName, palette, userId } = 15 | (await req.json()) as ThemeConfig & { userId: string }; 16 | 17 | try { 18 | const theme = await prisma.themes.update({ 19 | where: { 20 | xata_id: id, 21 | User: userId, 22 | }, 23 | data: { 24 | name, 25 | display_name: displayName, 26 | category: "user", 27 | ThemePalettes: { 28 | update: [ 29 | { 30 | where: { 31 | xata_id: palette.light.id, 32 | mode: "light", 33 | }, 34 | data: invertPalette(palette.light), 35 | }, 36 | { 37 | where: { 38 | xata_id: palette.dark.id, 39 | mode: "dark", 40 | }, 41 | data: invertPalette(palette.dark), 42 | }, 43 | ], 44 | }, 45 | }, 46 | include: { 47 | ThemePalettes: true, 48 | TokenColors: true, 49 | }, 50 | }); 51 | 52 | revalidatePath("/", "page"); 53 | 54 | return NextResponse.json(theme); 55 | } catch (error) { 56 | console.error("Error updating theme:", error); 57 | return NextResponse.json( 58 | { error: "Failed to update theme" }, 59 | { status: 500 } 60 | ); 61 | } finally { 62 | await prisma.$disconnect(); 63 | } 64 | }; 65 | 66 | export async function DELETE( 67 | req: Request, 68 | { params }: { params: { id: string } } 69 | ) { 70 | const { id } = params; 71 | const { userId } = await req.json(); 72 | 73 | if (!userId) { 74 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 75 | } 76 | 77 | try { 78 | await prisma.themes.delete({ 79 | where: { 80 | xata_id: id, 81 | User: userId, 82 | }, 83 | }); 84 | 85 | revalidatePath("/"); 86 | 87 | return NextResponse.json({ message: "Theme deleted successfully" }); 88 | } catch (error) { 89 | console.error("Error deleting theme:", error); 90 | return NextResponse.json( 91 | { error: "Failed to delete theme" }, 92 | { status: 500 } 93 | ); 94 | } finally { 95 | await prisma.$disconnect(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /apps/web/app/api/theme/[id]/status/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { revalidatePath } from "next/cache"; 3 | import { PrismaClient } from "@prisma/client"; 4 | 5 | const prisma = new PrismaClient(); 6 | 7 | export async function PATCH( 8 | req: Request, 9 | { params }: { params: { id: string } } 10 | ) { 11 | const { id } = params; 12 | const { isPublic, userId } = (await req.json()) as { 13 | isPublic: boolean; 14 | userId: string; 15 | }; 16 | 17 | if (!userId) { 18 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 19 | } 20 | 21 | try { 22 | const theme = await prisma.themes.update({ 23 | where: { 24 | xata_id: id, 25 | User: userId, 26 | }, 27 | data: { 28 | is_public: isPublic, 29 | }, 30 | }); 31 | 32 | revalidatePath("/"); 33 | 34 | return NextResponse.json(theme); 35 | } catch (error) { 36 | console.error("Error updating theme public status:", error); 37 | return NextResponse.json( 38 | { error: "Failed to update theme public status" }, 39 | { status: 500 } 40 | ); 41 | } finally { 42 | await prisma.$disconnect(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apps/web/app/api/theme/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { Prisma, PrismaClient } from "@prisma/client"; 3 | import { 4 | formatTheme, 5 | invertPaletteWithoutId, 6 | invertTokenColors, 7 | sortThemes, 8 | } from "@/app/utils"; 9 | import { defaultThemeConfig } from "@/lib/core/config"; 10 | import { revalidatePath } from "next/cache"; 11 | import { ThemeConfig } from "@/lib/core/types"; 12 | 13 | const prisma = new PrismaClient(); 14 | 15 | export const GET = async () => { 16 | try { 17 | const themes = await prisma.themes.findMany({ 18 | include: { 19 | ThemePalettes: true, 20 | TokenColors: true, 21 | Users: true, 22 | }, 23 | }); 24 | 25 | const formattedThemes = sortThemes(themes.map(formatTheme)); 26 | 27 | return NextResponse.json(formattedThemes); 28 | } catch (error) { 29 | console.error("Error fetching themes:", error); 30 | return NextResponse.json( 31 | { error: "Failed to fetch themes" }, 32 | { status: 500 } 33 | ); 34 | } finally { 35 | await prisma.$disconnect(); 36 | } 37 | }; 38 | 39 | export const POST = async (req: Request) => { 40 | const { name, displayName, palette, userId } = 41 | (await req.json()) as ThemeConfig & { userId: string | undefined }; 42 | 43 | try { 44 | const themeData: Prisma.ThemesCreateInput = { 45 | name: name, 46 | display_name: displayName, 47 | category: "user", 48 | is_public: true, 49 | ThemePalettes: { 50 | create: [ 51 | { mode: "light", ...invertPaletteWithoutId(palette.light) }, 52 | { mode: "dark", ...invertPaletteWithoutId(palette.dark) }, 53 | ], 54 | }, 55 | TokenColors: { 56 | create: [invertTokenColors(defaultThemeConfig.tokenColors)], 57 | }, 58 | }; 59 | 60 | if (userId !== undefined) { 61 | themeData.Users = { 62 | connect: { xata_id: userId }, 63 | }; 64 | } 65 | 66 | const theme = await prisma.themes.create({ 67 | data: themeData, 68 | include: { 69 | ThemePalettes: true, 70 | TokenColors: true, 71 | Users: true, 72 | }, 73 | }); 74 | 75 | revalidatePath("/", "page"); 76 | 77 | return NextResponse.json(theme); 78 | } catch (error) { 79 | console.error("Error creating theme:", error); 80 | return NextResponse.json( 81 | { error: "Failed to create theme" }, 82 | { status: 500 } 83 | ); 84 | } finally { 85 | await prisma.$disconnect(); 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /apps/web/app/api/themes/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import { getInitialThemes } from "@/lib/api"; 3 | 4 | export async function GET(request: Request) { 5 | const { searchParams } = new URL(request.url); 6 | const page = Number(searchParams.get("page")) || 1; 7 | const limit = Number(searchParams.get("limit")) || 20; 8 | const category = searchParams.get("category") || undefined; 9 | const userId = searchParams.get("userId") || undefined; 10 | 11 | try { 12 | const themes = await getInitialThemes(page, limit, category, userId); 13 | return NextResponse.json(themes); 14 | } catch (error) { 15 | return NextResponse.json( 16 | { error: "Failed to fetch themes" }, 17 | { status: 500 }, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/api/unsubscribe/route.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from "@upstash/redis"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | const redis = Redis.fromEnv(); 5 | 6 | export async function GET(request: NextRequest) { 7 | const { searchParams } = new URL(request.url); 8 | const email = searchParams.get("email"); 9 | 10 | if (!email) { 11 | return NextResponse.json( 12 | { error: "Email parameter is missing" }, 13 | { status: 400 } 14 | ); 15 | } 16 | 17 | const hashKey = `subscriptions:${email}`; 18 | const field = "tinte"; 19 | 20 | const isSubscribed = await redis.hget(hashKey, field); 21 | if (!isSubscribed) { 22 | return NextResponse.json( 23 | { error: "Email is not subscribed" }, 24 | { status: 400 } 25 | ); 26 | } 27 | 28 | await redis.hdel(hashKey, field); 29 | 30 | return NextResponse.json( 31 | { message: "Email unsubscribed successfully" }, 32 | { status: 200 } 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 3% 10%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 20 14.3% 4.1%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 20 14.3% 4.1%; 13 | --primary: 240 3% 10%; 14 | --primary-foreground: 60 9.1% 97.8%; 15 | --secondary: 60 4.8% 95.9%; 16 | --secondary-foreground: 24 9.8% 10%; 17 | --muted: 60 4.8% 95.9%; 18 | --muted-foreground: 25 5.3% 44.7%; 19 | --accent: 60 4.8% 95.9%; 20 | --accent-foreground: 24 9.8% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 60 9.1% 97.8%; 23 | --border: 20 5.9% 90%; 24 | --input: 20 5.9% 90%; 25 | --ring: 20 14.3% 4.1%; 26 | --chart-1: 12 76% 61%; 27 | --chart-2: 173 58% 39%; 28 | --chart-3: 197 37% 24%; 29 | --chart-4: 43 74% 66%; 30 | --chart-5: 27 87% 67%; 31 | 32 | --radius: 0.5rem; 33 | --space: 0.25rem; 34 | --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); 35 | } 36 | 37 | .dark { 38 | --background: 240 3% 10%; 39 | --foreground: 0 0% 100%; 40 | --card: 240 3% 10%; 41 | --card-foreground: 0 0% 100%; 42 | --popover: 240 3% 10%; 43 | --popover-foreground: 0 0% 100%; 44 | --primary: 0 0% 100%; 45 | --primary-foreground: 240 3% 10%; 46 | --secondary: 240 3% 20%; 47 | --secondary-foreground: 0 0% 100%; 48 | --muted: 240 3% 20%; 49 | --muted-foreground: 240 5% 64.9%; 50 | --accent: 240 3% 20%; 51 | --accent-foreground: 0 0% 100%; 52 | --destructive: 0 62.8% 30.6%; 53 | --destructive-foreground: 0 0% 100%; 54 | --border: 240 3% 20%; 55 | --input: 240 3% 20%; 56 | --ring: 240 4.9% 83.9%; 57 | --chart-1: 12 76% 61%; 58 | --chart-2: 173 58% 59%; 59 | --chart-3: 197 37% 54%; 60 | --chart-4: 43 74% 66%; 61 | --chart-5: 27 87% 67%; 62 | } 63 | } 64 | 65 | @layer base { 66 | * { 67 | @apply border-border; 68 | } 69 | body { 70 | @apply bg-background text-foreground; 71 | font-feature-settings: 72 | "rlig" 1, 73 | "calt" 1; 74 | } 75 | 76 | html.dark .shiki, 77 | html.dark .shiki span { 78 | color: var(--shiki-dark) !important; 79 | background-color: var(--shiki-dark-bg) !important; 80 | } 81 | 82 | .custom-shadow { 83 | box-shadow: 84 | 0 0 10px 0 hsl(var(--background) / 0.8), 85 | 0 0 30px 10px hsl(var(--background) / 0.6), 86 | 0 0 60px 20px hsl(var(--background) / 0.4), 87 | 0 0 100px 40px hsl(var(--background) / 0.2); 88 | } 89 | } 90 | 91 | .cl-modalBackdrop { 92 | align-items: center !important; 93 | } 94 | -------------------------------------------------------------------------------- /apps/web/app/t/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable turbo/no-undeclared-env-vars */ 2 | import { Metadata } from "next"; 3 | import { getThemeById } from "@/lib/api"; 4 | import { ThemeContent } from "./theme-content"; 5 | 6 | interface ThemePageProps { 7 | params: { 8 | id: string; 9 | }; 10 | } 11 | 12 | export async function generateMetadata({ 13 | params, 14 | }: ThemePageProps): Promise { 15 | const theme = await getThemeById(params.id); 16 | 17 | if (!theme) { 18 | return { 19 | title: "Theme Not Found", 20 | }; 21 | } 22 | 23 | const ogImageUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/api/og?id=${params.id}`; 24 | 25 | return { 26 | title: `${theme.displayName} - Tinte VS Code Theme`, 27 | description: `Explore the ${theme.displayName} theme for VS Code, created by ${theme.user?.username || "Anonymous"}. A ${theme.category} theme that enhances your coding experience.`, 28 | openGraph: { 29 | title: `${theme.displayName} - Tinte VS Code Theme`, 30 | description: `Explore the ${theme.displayName} theme for VS Code, created by ${theme.user?.username || "Anonymous"}. A ${theme.category} theme that enhances your coding experience.`, 31 | url: `${process.env.NEXT_PUBLIC_BASE_URL}/t/${params.id}`, 32 | siteName: "Tinte", 33 | images: [ 34 | { 35 | url: ogImageUrl, 36 | width: 1200, 37 | height: 630, 38 | alt: `${theme.displayName} VS Code Theme`, 39 | }, 40 | ], 41 | locale: "en-US", 42 | type: "website", 43 | }, 44 | twitter: { 45 | card: "summary_large_image", 46 | title: `${theme.displayName} - Tinte VS Code Theme`, 47 | description: `Explore the ${theme.displayName} theme for VS Code, created by ${theme.user?.username || "Anonymous"}. A ${theme.category} theme that enhances your coding experience.`, 48 | images: [ogImageUrl], 49 | creator: "@raillyhugo", 50 | }, 51 | }; 52 | } 53 | 54 | export default async function ThemePage({ params }: ThemePageProps) { 55 | const theme = await getThemeById(params.id); 56 | 57 | return ; 58 | } 59 | -------------------------------------------------------------------------------- /apps/web/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /apps/web/components/achievement-banner.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { motion } from "framer-motion"; 3 | 4 | const MidudevLogo = () => ( 5 | 13 | 14 | 18 | 22 | 23 | 27 | 31 | 32 | 33 | 34 | ); 35 | 36 | export const AchievementBanner: React.FC = () => { 37 | return ( 38 | 44 |

45 | 🏆 We won 3rd place at the 46 | ▲ Vercel 47 | x 48 | 49 | 50 | Midudev 51 | 52 | Hackathon! 53 |

54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /apps/web/components/atom/rh-tinte-link-logo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { IconTinte } from "@/components/ui/icons"; 3 | import RHLogoIcon from "@/public/rh-logo.svg"; 4 | import Link from "next/link"; 5 | 6 | export function RHTinteLinkLogo() { 7 | return ( 8 |
9 | 15 | 16 | 17 | {"/"} 18 | 19 | 20 |

tinte

21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/components/atom/tinte-link-logo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { IconTinte } from "@/components/ui/icons"; 3 | import Link from "next/link"; 4 | 5 | export function TinteLinkLogo() { 6 | return ( 7 | 8 | 9 |

tinte

10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/components/circular-gradient.tsx: -------------------------------------------------------------------------------- 1 | import { Palette } from "@/lib/core/types"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | interface CircularGradientProps { 5 | className?: string; 6 | palette: Palette; 7 | } 8 | 9 | export const CircularGradient = ({ 10 | className, 11 | palette, 12 | }: CircularGradientProps) => { 13 | return ( 14 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /apps/web/components/color-picker-button.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Button } from "@/components/ui/button"; 3 | import { 4 | Popover, 5 | PopoverContent, 6 | PopoverTrigger, 7 | } from "@/components/ui/popover"; 8 | import { Colorful } from "@uiw/react-color"; 9 | import { hsvaToHex, hsvaToHsla } from "@uiw/color-convert"; 10 | import { IconChevronDown } from "./ui/icons"; 11 | import { useDebounce } from "@/lib/hooks/use-debounce"; 12 | import { cn } from "@/lib/utils"; 13 | 14 | interface ColorPickerButtonProps { 15 | color: { h: number; s: number; v: number; a: number }; 16 | onChange: (color: { h: number; s: number; v: number; a: number }) => void; 17 | disabled?: boolean; 18 | } 19 | const getTextColorFromHSL = (l: number) => { 20 | return l > 50 ? "#000000" : "#FFFFFF"; 21 | }; 22 | 23 | const ColorPickerButton = ({ 24 | color, 25 | onChange, 26 | disabled, 27 | }: ColorPickerButtonProps) => { 28 | const [isOpen, setIsOpen] = useState(false); 29 | const [localColor, setLocalColor] = useState(color); 30 | 31 | const debouncedOnChange = useDebounce( 32 | (newColor: { h: number; s: number; v: number; a: number }) => { 33 | onChange(newColor); 34 | }, 35 | 100 36 | ); 37 | 38 | const handleColorChange = (newColor: { 39 | hsva: { h: number; s: number; v: number; a: number }; 40 | }) => { 41 | setLocalColor(newColor.hsva); 42 | debouncedOnChange(newColor.hsva); 43 | }; 44 | 45 | useEffect(() => { 46 | setLocalColor(color); 47 | }, [color]); 48 | 49 | const hsla = hsvaToHsla(localColor); 50 | const hslString = `${Math.round(hsla.h)}° ${Math.round(hsla.s)}% ${Math.round(hsla.l)}%`; 51 | 52 | const backgroundColor = hsvaToHex(localColor); 53 | const textColor = getTextColorFromHSL(hsla.l); 54 | 55 | return ( 56 | !disabled && setIsOpen(open)} 59 | > 60 | 61 | 73 | 74 | 75 | 80 | 81 | 82 | ); 83 | }; 84 | 85 | export default ColorPickerButton; 86 | -------------------------------------------------------------------------------- /apps/web/components/counterscale-script.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Script from "next/script"; 4 | 5 | export default function CounterscaleScript() { 6 | return ( 7 | <> 8 | 17 |