├── .gitignore ├── .vscode └── setting.json ├── README.md ├── biome.json ├── components.json ├── drizzle.config.ts ├── drizzle ├── 0000_talented_wasp.sql ├── 0001_colorful_nick_fury.sql ├── 0002_goofy_champions.sql ├── 0003_chunky_expediter.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ ├── 0003_snapshot.json │ ├── 0004_snapshot.json │ ├── 0005_snapshot.json │ └── _journal.json ├── env.d.ts ├── globals.d.ts ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── logo.svg ├── logos │ ├── apple_tv.svg │ ├── chatGPT.svg │ ├── hulu.svg │ ├── netflix.svg │ ├── spotify.svg │ └── youtube.svg └── screenshot.png ├── src ├── app │ ├── (app) │ │ ├── analytics │ │ │ └── page.tsx │ │ ├── dashboard │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (auth) │ │ └── login │ │ │ └── page.tsx │ ├── api │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ └── hello │ │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ ├── not-found.tsx │ └── page.tsx ├── components │ ├── analytics │ │ ├── analytics.tsx │ │ ├── graphs │ │ │ ├── analytics.tsx │ │ │ └── pichart.tsx │ │ └── subscription-analytics.tsx │ ├── calendar │ │ ├── add-subscription-content.tsx │ │ ├── add-subscription-dialog.tsx │ │ ├── calendar-grid.tsx │ │ ├── calendar-header.tsx │ │ ├── subscriotion-tracker.tsx │ │ └── subscription-details.tsx │ ├── dashboard │ │ ├── expense-breakdown.tsx │ │ ├── signout.tsx │ │ └── subscription-tracker.tsx │ ├── layout │ │ ├── provider.tsx │ │ └── themeprovider.tsx │ ├── logo │ │ ├── adobe-animate.tsx │ │ ├── adobe-xd.tsx │ │ ├── adobe.tsx │ │ ├── after-effects.tsx │ │ ├── apple.tsx │ │ ├── aws.tsx │ │ ├── builder-io.tsx │ │ ├── canva.tsx │ │ ├── claude.tsx │ │ ├── coursera.tsx │ │ ├── disney-plus.tsx │ │ ├── figma.tsx │ │ ├── firebase.tsx │ │ ├── fly.tsx │ │ ├── game.tsx │ │ ├── gemini.tsx │ │ ├── godaddy.tsx │ │ ├── hostgater.tsx │ │ ├── hulu.tsx │ │ ├── illustrator.tsx │ │ ├── indesign.tsx │ │ ├── kick.tsx │ │ ├── light-room.tsx │ │ ├── linkedin.tsx │ │ ├── mintlify.tsx │ │ ├── mongodb.tsx │ │ ├── neon-db.tsx │ │ ├── netflix.tsx │ │ ├── netlify.tsx │ │ ├── notion.tsx │ │ ├── openai.tsx │ │ ├── perplexity.tsx │ │ ├── photoshop.tsx │ │ ├── planetscale.tsx │ │ ├── premiere.tsx │ │ ├── prime-video.tsx │ │ ├── sketch.tsx │ │ ├── spotify.tsx │ │ ├── stability-ai.tsx │ │ ├── supabase.tsx │ │ ├── turso.tsx │ │ ├── twitch.tsx │ │ ├── twitter.tsx │ │ ├── udacity.tsx │ │ ├── udemy.tsx │ │ ├── upstash.tsx │ │ ├── vercel.tsx │ │ ├── webflow.tsx │ │ └── youtube.tsx │ ├── menu │ │ ├── menu.tsx │ │ └── useClickOutside.tsx │ └── ui │ │ ├── alert-dialog.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── chart.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── currency-input.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── radio-group.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ └── tabs.tsx ├── hooks │ └── use-media-query.tsx ├── lib │ ├── currencies.ts │ ├── platforms.tsx │ └── utils.ts ├── middleware.ts └── server │ ├── actions │ ├── auth-actions.ts │ └── subscriptions.ts │ ├── auth.ts │ └── db │ ├── index.ts │ └── schema.ts ├── tailwind.config.ts ├── tsconfig.json └── wrangler.toml /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # wrangler files 39 | .wrangler 40 | .dev.vars 41 | -------------------------------------------------------------------------------- /.vscode/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "biomejs.biome", 4 | "editor.codeActionsOnSave": { 5 | "quickfix.biome": "explicit", 6 | "source.organizeImports.biome": "explicit" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resubs - Subscription Tracking Platform 2 | 3 | Resubs is a modern, fast, and efficient subscription tracking platform that helps you manage and monitor your various digital subscriptions like Netflix, ChatGPT, YouTube, Spotify, and Apple services. Built with performance in mind, Resubs is hosted on Cloudflare's edge network and utilizes Cloudflare D1 for lightning-fast database operations. 4 | 5 | ## Features 6 | 7 | - Track multiple subscriptions from popular platforms 8 | - Visual calendar interface for easy subscription management 9 | - Analytics dashboard to monitor spending patterns 10 | - Fast and responsive design powered by Next.js and Cloudflare 11 | - Secure authentication using NextAuth.js 12 | 13 | ## Tech Stack 14 | 15 | - Next.js 16 | - React 17 | - TypeScript 18 | - Cloudflare Pages 19 | - Cloudflare D1 Database 20 | - NextAuth.js for authentication 21 | - Tailwind CSS for styling 22 | 23 | ## Setup Instructions 24 | 25 | Follow these steps to set up Resubs on your local machine and deploy it to Cloudflare: 26 | 27 | 1. Clone the repository: 28 | ``` 29 | git clone https://github.com/swarajbachu/resubs.git 30 | cd resubs 31 | ``` 32 | 33 | 2. Install dependencies: 34 | ``` 35 | pnpm install 36 | ``` 37 | 38 | 3. Set up Cloudflare: 39 | - Run `pnpx wrangler whoami` to ensure you're logged into your Cloudflare account 40 | - Create a new Cloudflare Pages project 41 | - Create a new D1 database for your project 42 | 43 | 4. Update your `wrangler.toml` file with the appropriate values for your Cloudflare project and D1 database. 44 | 45 | 5. Run database migrations: 46 | - Local: `pnpm run db:migrate:local` 47 | - Remote: `pnpm run db:migrate:prod` 48 | 49 | 6. Set up authentication: 50 | - Create a Google OAuth client and get the client ID and secret 51 | - Generate an AUTH_SECRET for NextAuth.js using `openssl` 52 | ``` 53 | openssl rand -base64 32 54 | ``` 55 | - Add these secrets to your Cloudflare Pages project: 56 | - AUTH_SECRET 57 | - AUTH_GOOGLE_ID 58 | - AUTH_GOOGLE_SECRET 59 | 60 | 7. Deploy to Cloudflare Pages: 61 | ``` 62 | pnpm run deploy 63 | ``` 64 | 65 | ## Local Development 66 | 67 | To run the project locally: 68 | 69 | 1. Start the development server: 70 | ``` 71 | pnpm run dev 72 | ``` 73 | 74 | 2. Open [http://localhost:3000](http://localhost:3000) in your browser to see the application. 75 | 76 | ## Contributing 77 | 78 | Contributions are welcome! Please feel free to submit a Pull Request. 79 | 80 | ## License 81 | 82 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 83 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", 3 | "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, 4 | "files": { 5 | "ignoreUnknown": false, 6 | "ignore": ["node_modules", ".next", ".vercel", ".wrangler", "public/"] 7 | }, 8 | "formatter": { "enabled": true, "indentStyle": "space" }, 9 | "organizeImports": { "enabled": true }, 10 | "linter": { 11 | "enabled": true, 12 | "rules": { 13 | "recommended": true, 14 | "a11y": { 15 | "noAriaUnsupportedElements": "warn", 16 | "noBlankTarget": "off", 17 | "useAltText": "warn", 18 | "useAriaPropsForRole": "warn", 19 | "useValidAriaProps": "warn", 20 | "useValidAriaValues": "warn", 21 | "noSvgWithoutTitle": "off" 22 | }, 23 | "correctness": { 24 | "noChildrenProp": "error", 25 | "useExhaustiveDependencies": "warn", 26 | "useHookAtTopLevel": "error", 27 | "useJsxKeyInIterable": "error" 28 | }, 29 | "security": { 30 | "noDangerouslySetInnerHtmlWithChildren": "error", 31 | "noDangerouslySetInnerHtml": "warn" 32 | }, 33 | "suspicious": { "noCommentText": "error", "noDuplicateJsxProps": "error" } 34 | } 35 | }, 36 | "javascript": { "formatter": { "quoteStyle": "double" } }, 37 | "overrides": [{ "include": ["**/*.ts?(x)"] }] 38 | } 39 | -------------------------------------------------------------------------------- /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.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | import fs from "node:fs"; 3 | import path from "node:path"; 4 | 5 | function getLocalD1DB() { 6 | try { 7 | const basePath = path.resolve(".wrangler"); 8 | const dbFile = fs 9 | .readdirSync(basePath, { encoding: "utf-8", recursive: true }) 10 | .find((f) => f.endsWith(".sqlite")); 11 | 12 | if (!dbFile) { 13 | throw new Error(`.sqlite file not found in ${basePath}`); 14 | } 15 | 16 | const url = path.resolve(basePath, dbFile); 17 | return url; 18 | } catch (err) { 19 | console.log(`Error ${err}`); 20 | } 21 | } 22 | 23 | export default defineConfig({ 24 | dialect: "sqlite", 25 | schema: "./src/server/db/schema.ts", 26 | out: "./drizzle", 27 | ...(process.env.NODE_ENV === "production" 28 | ? { 29 | driver: "d1-http", 30 | dbCredentials: { 31 | accountId: process.env.CLOUDFLARE_D1_ACCOUNT_ID, 32 | databaseId: process.env.DATABASE, 33 | token: process.env.CLOUDFLARE_D1_API_TOKEN, 34 | }, 35 | } 36 | : { 37 | dbCredentials: { 38 | url: getLocalD1DB(), 39 | }, 40 | }), 41 | }); 42 | -------------------------------------------------------------------------------- /drizzle/0000_talented_wasp.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `account` ( 2 | `userId` text NOT NULL, 3 | `type` text NOT NULL, 4 | `provider` text(255) NOT NULL, 5 | `providerAccountId` text(255) NOT NULL, 6 | `refresh_token` text(255), 7 | `access_token` text, 8 | `expires_at` integer, 9 | `token_type` text(255), 10 | `scope` text(255), 11 | `id_token` text, 12 | `session_state` text(255), 13 | PRIMARY KEY(`provider`, `providerAccountId`), 14 | FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade 15 | ); 16 | --> statement-breakpoint 17 | CREATE TABLE `authenticator` ( 18 | `credentialID` text NOT NULL, 19 | `userId` text NOT NULL, 20 | `providerAccountId` text NOT NULL, 21 | `credentialPublicKey` text NOT NULL, 22 | `counter` integer NOT NULL, 23 | `credentialDeviceType` text NOT NULL, 24 | `credentialBackedUp` integer NOT NULL, 25 | `transports` text, 26 | PRIMARY KEY(`userId`, `credentialID`), 27 | FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade 28 | ); 29 | --> statement-breakpoint 30 | CREATE TABLE `session` ( 31 | `sessionToken` text(255) PRIMARY KEY NOT NULL, 32 | `userId` text NOT NULL, 33 | `expires` integer NOT NULL, 34 | FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade 35 | ); 36 | --> statement-breakpoint 37 | CREATE TABLE `user` ( 38 | `id` text PRIMARY KEY NOT NULL, 39 | `name` text(255), 40 | `email` text(255) NOT NULL, 41 | `emailVerified` integer, 42 | `image` text(255), 43 | `created` integer NOT NULL, 44 | `updatedAt` integer NOT NULL 45 | ); 46 | --> statement-breakpoint 47 | CREATE TABLE `verificationToken` ( 48 | `identifier` text NOT NULL, 49 | `token` text NOT NULL, 50 | `expires` integer NOT NULL, 51 | PRIMARY KEY(`identifier`, `token`) 52 | ); 53 | --> statement-breakpoint 54 | CREATE UNIQUE INDEX `authenticator_credentialID_unique` ON `authenticator` (`credentialID`); -------------------------------------------------------------------------------- /drizzle/0001_colorful_nick_fury.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `reminders` ( 2 | `id` text PRIMARY KEY NOT NULL, 3 | `subscription_id` integer NOT NULL, 4 | `reminder_date` integer NOT NULL, 5 | `reminder_type` text NOT NULL, 6 | `is_acknowledged` integer DEFAULT false NOT NULL, 7 | `created_at` integer DEFAULT '"2024-09-21T10:33:32.018Z"' NOT NULL, 8 | `updated_at` integer DEFAULT '"2024-09-21T10:33:32.018Z"' NOT NULL 9 | ); 10 | --> statement-breakpoint 11 | CREATE TABLE `subscriptions` ( 12 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 13 | `name` text NOT NULL, 14 | `description` text, 15 | `price` text NOT NULL, 16 | `billing_cycle` text NOT NULL, 17 | `start_date` integer NOT NULL, 18 | `end_date` integer, 19 | `trial_end_date` integer, 20 | `is_active` integer DEFAULT true NOT NULL, 21 | `user_id` integer NOT NULL, 22 | `created_at` integer DEFAULT '"2024-09-21T10:33:32.018Z"' NOT NULL, 23 | `updated_at` integer DEFAULT '"2024-09-21T10:33:32.018Z"' NOT NULL 24 | ); 25 | -------------------------------------------------------------------------------- /drizzle/0002_goofy_champions.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE `subscriptions` RENAME COLUMN `description` TO `platform`;--> statement-breakpoint 2 | /* 3 | SQLite does not support "Set default to column" out of the box, we do not generate automatic migration for that, so it has to be done manually 4 | Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php 5 | https://www.sqlite.org/lang_altertable.html 6 | https://stackoverflow.com/questions/2083543/modify-a-columns-type-in-sqlite3 7 | 8 | Due to that we don't generate migration automatically and it has to be done manually 9 | */--> statement-breakpoint 10 | /* 11 | SQLite does not support "Set not null to column" out of the box, we do not generate automatic migration for that, so it has to be done manually 12 | Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php 13 | https://www.sqlite.org/lang_altertable.html 14 | https://stackoverflow.com/questions/2083543/modify-a-columns-type-in-sqlite3 15 | 16 | Due to that we don't generate migration automatically and it has to be done manually 17 | */--> statement-breakpoint 18 | /* 19 | SQLite does not support "Changing existing column type" out of the box, we do not generate automatic migration for that, so it has to be done manually 20 | Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php 21 | https://www.sqlite.org/lang_altertable.html 22 | https://stackoverflow.com/questions/2083543/modify-a-columns-type-in-sqlite3 23 | 24 | Due to that we don't generate migration automatically and it has to be done manually 25 | */--> statement-breakpoint 26 | /* 27 | SQLite does not support "Creating foreign key on existing column" out of the box, we do not generate automatic migration for that, so it has to be done manually 28 | Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php 29 | https://www.sqlite.org/lang_altertable.html 30 | 31 | Due to that we don't generate migration automatically and it has to be done manually 32 | */ -------------------------------------------------------------------------------- /drizzle/0003_chunky_expediter.sql: -------------------------------------------------------------------------------- 1 | /* 2 | SQLite does not support "Set default to column" out of the box, we do not generate automatic migration for that, so it has to be done manually 3 | Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php 4 | https://www.sqlite.org/lang_altertable.html 5 | https://stackoverflow.com/questions/2083543/modify-a-columns-type-in-sqlite3 6 | 7 | Due to that we don't generate migration automatically and it has to be done manually 8 | */--> statement-breakpoint 9 | /* 10 | SQLite does not support "Changing existing column type" out of the box, we do not generate automatic migration for that, so it has to be done manually 11 | Please refer to: https://www.techonthenet.com/sqlite/tables/alter_table.php 12 | https://www.sqlite.org/lang_altertable.html 13 | https://stackoverflow.com/questions/2083543/modify-a-columns-type-in-sqlite3 14 | 15 | Due to that we don't generate migration automatically and it has to be done manually 16 | */--> statement-breakpoint 17 | ALTER TABLE `subscriptions` ADD `currency` text DEFAULT 'USD' NOT NULL; -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1726910580483, 9 | "tag": "0000_talented_wasp", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "6", 15 | "when": 1726914812024, 16 | "tag": "0001_colorful_nick_fury", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "6", 22 | "when": 1726976739058, 23 | "tag": "0002_goofy_champions", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "6", 29 | "when": 1727123158402, 30 | "tag": "0003_chunky_expediter", 31 | "breakpoints": true 32 | }, 33 | { 34 | "idx": 4, 35 | "version": "6", 36 | "when": 1727124767915, 37 | "tag": "0004_skinny_gabe_jones", 38 | "breakpoints": true 39 | }, 40 | { 41 | "idx": 5, 42 | "version": "6", 43 | "when": 1727125038352, 44 | "tag": "0005_chilly_doorman", 45 | "breakpoints": true 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv env.d.ts` 2 | 3 | interface CloudflareEnv { 4 | AUTH_GOOGLE_ID: string; 5 | AUTH_GOOGLE_SECRET: string; 6 | NEXTAUTH_URL: string; 7 | AUTH_SECRET: string; 8 | DB: D1Database; 9 | } 10 | -------------------------------------------------------------------------------- /globals.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv extends CloudflareEnv {} 4 | } 5 | } 6 | 7 | export type {}; 8 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import { setupDevPlatform } from "@cloudflare/next-on-pages/next-dev"; 2 | 3 | // Here we use the @cloudflare/next-on-pages next-dev module to allow us to use bindings during local development 4 | // (when running the application with `next dev`), for more information see: 5 | // https://github.com/cloudflare/next-on-pages/blob/main/internal-packages/next-dev/README.md 6 | if (process.env.NODE_ENV === "development") { 7 | await setupDevPlatform(); 8 | } 9 | 10 | /** @type {import('next').NextConfig} */ 11 | const nextConfig = { 12 | images: { 13 | remotePatterns: [{ hostname: "images.unsplash.com" }], 14 | }, 15 | }; 16 | 17 | export default nextConfig; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remainder-calendar", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "pnpm biome lint .", 10 | "format": "pnpm biome format .", 11 | "lint:fix": "pnpm biome lint --write .", 12 | "format:fix": "pnpm biome format --write .", 13 | "db:generate": "drizzle-kit generate", 14 | "db:studio": "drizzle-kit studio", 15 | "db:migrate:local": "wrangler d1 migrations apply resubs --local ", 16 | "db:studio:local": "wrangler d1 migrations apply resubs --local && drizzle-kit studio", 17 | "db:migrate:prod": "wrangler d1 migrations apply resubs --remote", 18 | "pages:build": "pnpm next-on-pages", 19 | "preview": "pnpm pages:build && wrangler pages dev", 20 | "deploy": "pnpm pages:build && wrangler pages deploy", 21 | "cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts" 22 | }, 23 | "dependencies": { 24 | "@auth/core": "^0.35.0", 25 | "@auth/drizzle-adapter": "^1.5.0", 26 | "@radix-ui/react-alert-dialog": "^1.1.1", 27 | "@radix-ui/react-checkbox": "^1.1.1", 28 | "@radix-ui/react-dialog": "^1.1.1", 29 | "@radix-ui/react-icons": "^1.3.0", 30 | "@radix-ui/react-label": "^2.1.0", 31 | "@radix-ui/react-popover": "^1.1.1", 32 | "@radix-ui/react-radio-group": "^1.2.0", 33 | "@radix-ui/react-scroll-area": "^1.1.0", 34 | "@radix-ui/react-select": "^2.1.1", 35 | "@radix-ui/react-separator": "^1.1.0", 36 | "@radix-ui/react-slot": "^1.1.0", 37 | "@radix-ui/react-switch": "^1.1.0", 38 | "@radix-ui/react-tabs": "^1.1.0", 39 | "@tanstack/react-query": "^5.56.2", 40 | "better-sqlite3": "^11.3.0", 41 | "class-variance-authority": "^0.7.0", 42 | "clsx": "^2.1.1", 43 | "cmdk": "1.0.0", 44 | "date-fns": "^4.1.0", 45 | "drizzle-orm": "^0.33.0", 46 | "drizzle-zod": "^0.5.1", 47 | "framer-motion": "^11.5.6", 48 | "googleapis": "^144.0.0", 49 | "id": "^0.0.0", 50 | "lucide-react": "^0.441.0", 51 | "nano": "^10.1.4", 52 | "next": "14.2.5", 53 | "next-auth": "5.0.0-beta.21", 54 | "next-themes": "^0.3.0", 55 | "react": "^18", 56 | "react-day-picker": "8.10.1", 57 | "react-dom": "^18", 58 | "recharts": "^2.12.7", 59 | "sonner": "^1.5.0", 60 | "tailwind-merge": "^2.5.2", 61 | "tailwindcss-animate": "^1.0.7", 62 | "vaul": "^0.9.4", 63 | "zod": "^3.23.8" 64 | }, 65 | "devDependencies": { 66 | "@biomejs/biome": "1.9.2", 67 | "@cloudflare/next-on-pages": "1", 68 | "@cloudflare/workers-types": "^4.20240919.0", 69 | "@types/node": "^20", 70 | "@types/react": "^18", 71 | "@types/react-dom": "^18", 72 | "drizzle-kit": "^0.24.2", 73 | "postcss": "^8", 74 | "tailwindcss": "^3.4.1", 75 | "typescript": "^5", 76 | "vercel": "^37.5.2", 77 | "wrangler": "^3.78.6" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/logos/apple_tv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/logos/chatGPT.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/logos/hulu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/logos/netflix.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/logos/spotify.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/logos/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swarajbachu/resubs/1a5e76d76b65f02cd8f53c413f402444d5429776/public/screenshot.png -------------------------------------------------------------------------------- /src/app/(app)/analytics/page.tsx: -------------------------------------------------------------------------------- 1 | import { SubscriptionAnalytics } from "@/components/analytics/graphs/analytics"; 2 | import React from "react"; 3 | 4 | export default function Analytics() { 5 | return ( 6 |
7 |

Analytics

8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/(app)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { SubscriptionTracker } from "@/components/calendar/subscriotion-tracker"; 2 | import React from "react"; 3 | 4 | export default function page() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Menu from "@/components/menu/menu"; 2 | import type React from "react"; 3 | 4 | export default function layout({ children }: { children: React.ReactNode }) { 5 | return ( 6 |
7 | {children} 8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { signIn } from "@/server/auth"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export default async function LoginPage() { 7 | return ( 8 |
9 |
10 |
{" "} 11 |
12 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/api/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from "@/server/auth"; 2 | export const runtime = "edge"; 3 | -------------------------------------------------------------------------------- /src/app/api/hello/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server"; 2 | import { getRequestContext } from "@cloudflare/next-on-pages"; 3 | 4 | export const runtime = "edge"; 5 | 6 | export async function GET(request: NextRequest) { 7 | const responseText = "Hello World"; 8 | 9 | // In the edge runtime you can use Bindings that are available in your application 10 | // (for more details see: 11 | // - https://developers.cloudflare.com/pages/framework-guides/deploy-a-nextjs-site/#use-bindings-in-your-nextjs-application 12 | // - https://developers.cloudflare.com/pages/functions/bindings/ 13 | // ) 14 | // 15 | // KV Example: 16 | // const myKv = getRequestContext().env.MY_KV_NAMESPACE 17 | // await myKv.put('suffix', ' from a KV store!') 18 | // const suffix = await myKv.get('suffix') 19 | // responseText += suffix 20 | 21 | return new Response(responseText); 22 | } 23 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swarajbachu/resubs/1a5e76d76b65f02cd8f53c413f402444d5429776/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 50 17% 95%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 50 23% 97%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 50 23% 97%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 6% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --neutral: 240 5.9% 90%; 20 | ---neutral-foreground: 240 3.7% 15.9%; 21 | --accent: 240 4.8% 95.9%; 22 | --accent-foreground: 240 5.9% 10%; 23 | --destructive: 0 84.2% 60.2%; 24 | --destructive-foreground: 0 0% 98%; 25 | --border: 240 5.9% 90%; 26 | --input: 240 5.9% 90%; 27 | --ring: 240 5.9% 10%; 28 | --radius: 0.9rem; 29 | --chart-1: 12 76% 61%; 30 | --chart-2: 173 58% 39%; 31 | --chart-3: 197 37% 24%; 32 | --chart-4: 43 74% 66%; 33 | --chart-5: 27 87% 67%; 34 | } 35 | 36 | .dark { 37 | --background: 240 10% 3.9%; 38 | --foreground: 0 0% 98%; 39 | --card: 240 5.9% 10%; 40 | --card-foreground: 0 0% 98%; 41 | --popover: 240 5.9% 10%; 42 | --popover-foreground: 0 0% 98%; 43 | --primary: 0 0% 98%; 44 | --primary-foreground: 240 5.9% 10%; 45 | --secondary: 240 3.7% 15.9%; 46 | --secondary-foreground: 0 0% 98%; 47 | --muted: 240 3.7% 15.9%; 48 | --muted-foreground: 240 5% 64.9%; 49 | --accent: 240 3.7% 15.9%; 50 | --accent-foreground: 0 0% 98%; 51 | --destructive: 0 62.8% 30.6%; 52 | --destructive-foreground: 0 0% 98%; 53 | --border: 240 3.7% 15.9%; 54 | --input: 240 3.7% 15.9%; 55 | --ring: 240 4.9% 83.9%; 56 | --chart-1: 220 70% 50%; 57 | --chart-2: 160 60% 45%; 58 | --chart-3: 30 80% 55%; 59 | --chart-4: 280 65% 60%; 60 | --chart-5: 340 75% 55%; 61 | } 62 | } 63 | 64 | @layer base { 65 | * { 66 | @apply border-border; 67 | } 68 | body { 69 | @apply !bg-background text-foreground; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { Toaster } from "sonner"; 5 | import QueryProvider from "@/components/layout/provider"; 6 | import { ThemeProvider } from "@/components/layout/themeprovider"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Resubs", 12 | description: 13 | "Resubs is a subscription management tool that helps you track your subscriptions and save money.", 14 | }; 15 | 16 | export const runtime = "edge"; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: Readonly<{ 21 | children: React.ReactNode; 22 | }>) { 23 | return ( 24 | 25 | 31 | 32 | 33 | 34 | {children} 35 | 36 | 37 | 38 | 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | export const runtime = "edge"; 2 | 3 | export default function NotFound() { 4 | return ( 5 | <> 6 | 404: This page could not be found. 7 |
8 |
9 | 17 | 23 | 27 | 31 | 32 | ); 33 | export default AmazonWebServices; 34 | -------------------------------------------------------------------------------- /src/components/logo/builder-io.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Builder = (props: SVGProps) => ( 4 | 10 | 14 | 18 | 22 | 23 | ); 24 | export default Builder; 25 | -------------------------------------------------------------------------------- /src/components/logo/canva.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Canva = (props: SVGProps) => ( 4 | 10 | 11 | 15 | 19 | 23 | 27 | 31 | 35 | 36 | 37 | 45 | 46 | 47 | 48 | 56 | 57 | 58 | 59 | 67 | 68 | 69 | 70 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | export default Canva; 89 | -------------------------------------------------------------------------------- /src/components/logo/claude.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const ClaudeAI = (props: SVGProps) => ( 4 | 14 | 15 | 20 | 21 | ); 22 | export default ClaudeAI; 23 | -------------------------------------------------------------------------------- /src/components/logo/coursera.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Coursera = (props: SVGProps) => ( 4 | 13 | 18 | 19 | ); 20 | export default Coursera; 21 | -------------------------------------------------------------------------------- /src/components/logo/figma.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Figma = (props: SVGProps) => ( 4 | 10 | 11 | 15 | 19 | 23 | 27 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | ); 39 | export default Figma; 40 | -------------------------------------------------------------------------------- /src/components/logo/firebase.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Firebase = (props: SVGProps) => ( 4 | 10 | 14 | 18 | 22 | 26 | 27 | ); 28 | export default Firebase; 29 | -------------------------------------------------------------------------------- /src/components/logo/game.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import React from "react"; 3 | 4 | export default function GameLogo({ className }: { className?: string }) { 5 | return ( 6 | <> 7 | {/*?xml version="1.0" ?*/} 8 | 17 | 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/logo/gemini.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Gemini = (props: SVGProps) => ( 4 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 52 | 53 | ); 54 | export default Gemini; 55 | -------------------------------------------------------------------------------- /src/components/logo/godaddy.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const GoDaddy = (props: SVGProps) => ( 4 | 10 | 14 | 18 | 19 | ); 20 | export default GoDaddy; 21 | -------------------------------------------------------------------------------- /src/components/logo/hostgater.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Hostgator = (props: SVGProps) => ( 4 | 10 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 30 | 34 | 38 | 42 | 43 | 47 | 48 | 52 | 56 | 60 | 64 | 68 | 72 | 76 | 80 | 84 | 88 | 89 | 93 | 97 | 101 | 102 | ); 103 | export default Hostgator; 104 | -------------------------------------------------------------------------------- /src/components/logo/hulu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Hulu = (props: SVGProps) => ( 4 | 10 | 14 | 15 | ); 16 | export default Hulu; 17 | -------------------------------------------------------------------------------- /src/components/logo/illustrator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Illustrator = (props: SVGProps) => ( 4 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | export default Illustrator; 32 | -------------------------------------------------------------------------------- /src/components/logo/indesign.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const InDesign = (props: SVGProps) => ( 4 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | export default InDesign; 32 | -------------------------------------------------------------------------------- /src/components/logo/kick.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Kick = (props: SVGProps) => ( 4 | 10 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | export default Kick; 31 | -------------------------------------------------------------------------------- /src/components/logo/light-room.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Lightroom = (props: SVGProps) => ( 4 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | export default Lightroom; 32 | -------------------------------------------------------------------------------- /src/components/logo/linkedin.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const LinkedIn = (props: SVGProps) => ( 4 | 10 | 14 | 15 | ); 16 | export default LinkedIn; 17 | -------------------------------------------------------------------------------- /src/components/logo/mintlify.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Mintlify = (props: SVGProps) => ( 4 | 10 | 11 | 15 | 19 | 23 | 24 | ); 25 | export default Mintlify; 26 | -------------------------------------------------------------------------------- /src/components/logo/mongodb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const MongoDB = (props: SVGProps) => ( 4 | 10 | 14 | 15 | ); 16 | export default MongoDB; 17 | -------------------------------------------------------------------------------- /src/components/logo/neon-db.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Neon = (props: SVGProps) => ( 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 28 | 33 | 37 | 38 | ); 39 | export default Neon; 40 | -------------------------------------------------------------------------------- /src/components/logo/netflix.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Netflix = (props: SVGProps) => ( 4 | 10 | 11 | 20 | 21 | 22 | 23 | 24 | 25 | 31 | 35 | 39 | 45 | 49 | 53 | 54 | ); 55 | export default Netflix; 56 | -------------------------------------------------------------------------------- /src/components/logo/netlify.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Netlify = (props: SVGProps) => ( 4 | 10 | 14 | 18 | 22 | 23 | ); 24 | export default Netlify; 25 | -------------------------------------------------------------------------------- /src/components/logo/notion.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Notion = (props: SVGProps) => ( 4 | 10 | 14 | 15 | 16 | ); 17 | export default Notion; 18 | -------------------------------------------------------------------------------- /src/components/logo/openai.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const OpenAI = (props: SVGProps) => ( 4 | 10 | 11 | 12 | ); 13 | export default OpenAI; 14 | -------------------------------------------------------------------------------- /src/components/logo/perplexity.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const PerplexityAI = (props: SVGProps) => ( 4 | 5 | 12 | 19 | 26 | 27 | ); 28 | export default PerplexityAI; 29 | -------------------------------------------------------------------------------- /src/components/logo/photoshop.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Photoshop = (props: SVGProps) => ( 4 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | export default Photoshop; 32 | -------------------------------------------------------------------------------- /src/components/logo/planetscale.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const PlanetScale = (props: SVGProps) => ( 4 | 10 | 11 | 12 | ); 13 | export default PlanetScale; 14 | -------------------------------------------------------------------------------- /src/components/logo/premiere.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Premiere = (props: SVGProps) => ( 4 | 10 | 11 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | export default Premiere; 32 | -------------------------------------------------------------------------------- /src/components/logo/sketch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Sketch = (props: SVGProps) => ( 4 | 10 | 15 | 16 | ); 17 | export default Sketch; 18 | -------------------------------------------------------------------------------- /src/components/logo/spotify.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Spotify = (props: SVGProps) => ( 4 | 10 | 14 | 15 | ); 16 | export default Spotify; 17 | -------------------------------------------------------------------------------- /src/components/logo/stability-ai.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const StabilityAI = (props: SVGProps) => ( 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 24 | 25 | ); 26 | export default StabilityAI; 27 | -------------------------------------------------------------------------------- /src/components/logo/supabase.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Supabase = (props: SVGProps) => ( 4 | 10 | 14 | 19 | 23 | 24 | 32 | 33 | 34 | 35 | 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | export default Supabase; 50 | -------------------------------------------------------------------------------- /src/components/logo/turso.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Turso = (props: SVGProps) => ( 4 | 10 | 14 | 15 | ); 16 | export default Turso; 17 | -------------------------------------------------------------------------------- /src/components/logo/twitch.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Twitch = (props: SVGProps) => ( 4 | 14 | 15 | 21 | 22 | 26 | 30 | 31 | 32 | ); 33 | export default Twitch; 34 | -------------------------------------------------------------------------------- /src/components/logo/twitter.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const XformerlyTwitter = (props: SVGProps) => ( 4 | 10 | 14 | 15 | ); 16 | export default XformerlyTwitter; 17 | -------------------------------------------------------------------------------- /src/components/logo/udacity.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Udacity = (props: SVGProps) => ( 4 | 5 | 11 | 12 | ); 13 | export default Udacity; 14 | -------------------------------------------------------------------------------- /src/components/logo/udemy.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Udemy = (props: SVGProps) => ( 4 | 10 | 14 | 18 | 19 | ); 20 | export default Udemy; 21 | -------------------------------------------------------------------------------- /src/components/logo/upstash.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Upstash = (props: SVGProps) => ( 4 | 10 | 14 | 18 | 22 | 27 | 32 | 33 | ); 34 | export default Upstash; 35 | -------------------------------------------------------------------------------- /src/components/logo/vercel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const Vercel = (props: SVGProps) => ( 4 | 10 | 11 | 12 | ); 13 | export default Vercel; 14 | -------------------------------------------------------------------------------- /src/components/logo/webflow.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const WebFlow = (props: SVGProps) => ( 4 | 10 | 14 | 15 | ); 16 | export default WebFlow; 17 | -------------------------------------------------------------------------------- /src/components/logo/youtube.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import type { SVGProps } from "react"; 3 | const YouTube = (props: SVGProps) => ( 4 | 10 | 14 | 15 | 16 | ); 17 | export default YouTube; 18 | -------------------------------------------------------------------------------- /src/components/menu/menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useRef, useEffect, useId } from "react"; 4 | import { AnimatePresence, MotionConfig, motion } from "framer-motion"; 5 | import { Twitter, Moon, Sun, LogIn, LogOut, X, Menu } from "lucide-react"; 6 | import useClickOutside from "./useClickOutside"; 7 | import { Button } from "../ui/button"; 8 | import { useTheme } from "next-themes"; 9 | import Link from "next/link"; 10 | import { signOutAction } from "@/server/actions/auth-actions"; 11 | 12 | const TRANSITION = { 13 | type: "spring", 14 | bounce: 0.1, 15 | duration: 0.3, 16 | }; 17 | 18 | export default function Component() { 19 | const uniqueId = useId(); 20 | const menuRef = useRef(null); 21 | const [isOpen, setIsOpen] = useState(false); 22 | const { setTheme, theme } = useTheme(); 23 | const [isDarkMode, setIsDarkMode] = useState(theme === "dark"); 24 | const [isLoggedIn, setIsLoggedIn] = useState(false); 25 | 26 | const toggleMenu = () => setIsOpen(!isOpen); 27 | const toggleDarkMode = () => { 28 | setTheme(theme === "dark" ? "light" : "dark"); 29 | setIsDarkMode(!isDarkMode); 30 | }; 31 | 32 | useClickOutside(menuRef, () => { 33 | if (isOpen) setIsOpen(false); 34 | }); 35 | 36 | useEffect(() => { 37 | const handleKeyDown = (event: KeyboardEvent) => { 38 | if (event.key === "Escape" && isOpen) { 39 | setIsOpen(false); 40 | } 41 | }; 42 | document.addEventListener("keydown", handleKeyDown); 43 | return () => document.removeEventListener("keydown", handleKeyDown); 44 | }, [isOpen]); 45 | 46 | useEffect(() => { 47 | document.body.classList.toggle("dark", isDarkMode); 48 | }, [isDarkMode]); 49 | 50 | return ( 51 | 52 |
53 | 62 | {isOpen ? ( 63 |
64 |
65 | 69 | Menu 70 | 71 | 79 |
80 | 86 |
  • 87 | 91 | 92 | Share on Twitter 93 | 94 |
  • 95 |
  • 96 | 108 |
  • 109 |
  • 110 | 129 |
  • 130 |
    131 |
    132 | ) : ( 133 | 143 | )} 144 |
    145 |
    146 |
    147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /src/components/menu/useClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import { type RefObject, useEffect } from "react"; 2 | 3 | function useClickOutside( 4 | ref: RefObject, 5 | handler: (event: MouseEvent | TouchEvent) => void, 6 | ): void { 7 | useEffect(() => { 8 | const handleClickOutside = (event: MouseEvent | TouchEvent) => { 9 | if (!ref || !ref.current || ref.current.contains(event.target as Node)) { 10 | return; 11 | } 12 | 13 | handler(event); 14 | }; 15 | 16 | document.addEventListener("mousedown", handleClickOutside); 17 | document.addEventListener("touchstart", handleClickOutside); 18 | 19 | return () => { 20 | document.removeEventListener("mousedown", handleClickOutside); 21 | document.removeEventListener("touchstart", handleClickOutside); 22 | }; 23 | }, [ref, handler]); 24 | } 25 | 26 | export default useClickOutside; 27 | -------------------------------------------------------------------------------- /src/components/ui/alert-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | import { buttonVariants } from "@/components/ui/button"; 8 | 9 | const AlertDialog = AlertDialogPrimitive.Root; 10 | 11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger; 12 | 13 | const AlertDialogPortal = AlertDialogPrimitive.Portal; 14 | 15 | const AlertDialogOverlay = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, ...props }, ref) => ( 19 | 27 | )); 28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; 29 | 30 | const AlertDialogContent = React.forwardRef< 31 | React.ElementRef, 32 | React.ComponentPropsWithoutRef 33 | >(({ className, ...props }, ref) => ( 34 | 35 | 36 | 44 | 45 | )); 46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; 47 | 48 | const AlertDialogHeader = ({ 49 | className, 50 | ...props 51 | }: React.HTMLAttributes) => ( 52 |
    59 | ); 60 | AlertDialogHeader.displayName = "AlertDialogHeader"; 61 | 62 | const AlertDialogFooter = ({ 63 | className, 64 | ...props 65 | }: React.HTMLAttributes) => ( 66 |
    73 | ); 74 | AlertDialogFooter.displayName = "AlertDialogFooter"; 75 | 76 | const AlertDialogTitle = React.forwardRef< 77 | React.ElementRef, 78 | React.ComponentPropsWithoutRef 79 | >(({ className, ...props }, ref) => ( 80 | 85 | )); 86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; 87 | 88 | const AlertDialogDescription = React.forwardRef< 89 | React.ElementRef, 90 | React.ComponentPropsWithoutRef 91 | >(({ className, ...props }, ref) => ( 92 | 97 | )); 98 | AlertDialogDescription.displayName = 99 | AlertDialogPrimitive.Description.displayName; 100 | 101 | const AlertDialogAction = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )); 111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; 112 | 113 | const AlertDialogCancel = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 126 | )); 127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; 128 | 129 | export { 130 | AlertDialog, 131 | AlertDialogPortal, 132 | AlertDialogOverlay, 133 | AlertDialogTrigger, 134 | AlertDialogContent, 135 | AlertDialogHeader, 136 | AlertDialogFooter, 137 | AlertDialogTitle, 138 | AlertDialogDescription, 139 | AlertDialogAction, 140 | AlertDialogCancel, 141 | }; 142 | -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
    33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-6 py-6", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /src/components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type * as React from "react"; 4 | import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"; 5 | import { DayPicker } from "react-day-picker"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { buttonVariants } from "@/components/ui/button"; 9 | 10 | export type CalendarProps = React.ComponentProps; 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md" 43 | : "[&:has([aria-selected])]:rounded-md", 44 | ), 45 | day: cn( 46 | buttonVariants({ variant: "ghost" }), 47 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100", 48 | ), 49 | day_range_start: "day-range-start", 50 | day_range_end: "day-range-end", 51 | day_selected: 52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", 53 | day_today: "bg-accent text-accent-foreground", 54 | day_outside: 55 | "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30", 56 | day_disabled: "text-muted-foreground opacity-50", 57 | day_range_middle: 58 | "aria-selected:bg-accent aria-selected:text-accent-foreground", 59 | day_hidden: "invisible", 60 | ...classNames, 61 | }} 62 | components={{ 63 | IconLeft: ({ ...props }) => , 64 | IconRight: ({ ...props }) => , 65 | }} 66 | {...props} 67 | /> 68 | ); 69 | } 70 | Calendar.displayName = "Calendar"; 71 | 72 | export { Calendar }; 73 | -------------------------------------------------------------------------------- /src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    14 | )); 15 | Card.displayName = "Card"; 16 | 17 | const CardHeader = React.forwardRef< 18 | HTMLDivElement, 19 | React.HTMLAttributes 20 | >(({ className, ...props }, ref) => ( 21 |
    26 | )); 27 | CardHeader.displayName = "CardHeader"; 28 | 29 | const CardTitle = React.forwardRef< 30 | HTMLParagraphElement, 31 | React.HTMLAttributes 32 | >(({ className, ...props }, ref) => ( 33 |

    38 | )); 39 | CardTitle.displayName = "CardTitle"; 40 | 41 | const CardDescription = React.forwardRef< 42 | HTMLParagraphElement, 43 | React.HTMLAttributes 44 | >(({ className, ...props }, ref) => ( 45 |

    50 | )); 51 | CardDescription.displayName = "CardDescription"; 52 | 53 | const CardContent = React.forwardRef< 54 | HTMLDivElement, 55 | React.HTMLAttributes 56 | >(({ className, ...props }, ref) => ( 57 |

    58 | )); 59 | CardContent.displayName = "CardContent"; 60 | 61 | const CardFooter = React.forwardRef< 62 | HTMLDivElement, 63 | React.HTMLAttributes 64 | >(({ className, ...props }, ref) => ( 65 |
    70 | )); 71 | CardFooter.displayName = "CardFooter"; 72 | 73 | export { 74 | Card, 75 | CardHeader, 76 | CardFooter, 77 | CardTitle, 78 | CardDescription, 79 | CardContent, 80 | }; 81 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; 5 | import { CheckIcon } from "@radix-ui/react-icons"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )); 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName; 29 | 30 | export { Checkbox }; 31 | -------------------------------------------------------------------------------- /src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import type { DialogProps } from "@radix-ui/react-dialog"; 5 | import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; 6 | import { Command as CommandPrimitive } from "cmdk"; 7 | 8 | import { cn } from "@/lib/utils"; 9 | import { Dialog, DialogContent } from "@/components/ui/dialog"; 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )); 24 | Command.displayName = CommandPrimitive.displayName; 25 | 26 | interface CommandDialogProps extends DialogProps {} 27 | 28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
    45 | 46 | 54 |
    55 | )); 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName; 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )); 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName; 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 81 | )); 82 | 83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName; 84 | 85 | const CommandGroup = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )); 98 | 99 | CommandGroup.displayName = CommandPrimitive.Group.displayName; 100 | 101 | const CommandSeparator = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )); 111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName; 112 | 113 | const CommandItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 125 | )); 126 | 127 | CommandItem.displayName = CommandPrimitive.Item.displayName; 128 | 129 | const CommandShortcut = ({ 130 | className, 131 | ...props 132 | }: React.HTMLAttributes) => { 133 | return ( 134 | 141 | ); 142 | }; 143 | CommandShortcut.displayName = "CommandShortcut"; 144 | 145 | export { 146 | Command, 147 | CommandDialog, 148 | CommandInput, 149 | CommandList, 150 | CommandEmpty, 151 | CommandGroup, 152 | CommandItem, 153 | CommandShortcut, 154 | CommandSeparator, 155 | }; 156 | -------------------------------------------------------------------------------- /src/components/ui/currency-input.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Input } from "@/components/ui/input"; 3 | import { Button } from "@/components/ui/button"; 4 | import { 5 | Popover, 6 | PopoverContent, 7 | PopoverTrigger, 8 | } from "@/components/ui/popover"; 9 | import { 10 | Command, 11 | CommandEmpty, 12 | CommandGroup, 13 | CommandInput, 14 | CommandItem, 15 | CommandList, 16 | } from "@/components/ui/command"; 17 | import { Check } from "lucide-react"; 18 | import { currencyList, getUserCurrency } from "@/lib/currencies"; 19 | 20 | type Currency = { 21 | code: string; 22 | symbol: string; 23 | name: string; 24 | }; 25 | 26 | type PriceInputProps = { 27 | value: string; 28 | onChange: (value: string, currency: string) => void; 29 | currency: string; 30 | setCurrency: (currency: string) => void; 31 | }; 32 | 33 | export function PriceInput({ 34 | value, 35 | onChange, 36 | currency, 37 | setCurrency, 38 | }: PriceInputProps) { 39 | const [selectedCurrency, setSelectedCurrency] = useState( 40 | null, 41 | ); 42 | const [openCurrencySelect, setOpenCurrencySelect] = useState(false); 43 | 44 | useEffect(() => { 45 | const defaultCurrencyCode = 46 | currencyList.find((curr) => curr.code === currency)?.code || 47 | getUserCurrency(); 48 | const defaultCurrency = 49 | currencyList.find((currency) => currency.code === defaultCurrencyCode) || 50 | currencyList[0]; 51 | setSelectedCurrency(defaultCurrency); 52 | setCurrency(defaultCurrency.code); 53 | }, []); 54 | 55 | const handleCurrencySelect = (currency: Currency) => { 56 | setSelectedCurrency(currency); 57 | setOpenCurrencySelect(false); 58 | onChange(value, currency.code); 59 | setCurrency(currency.code); 60 | }; 61 | 62 | return ( 63 |
    64 | 69 | 70 | 78 | 79 | 80 | 81 | 82 | 83 | No currency found. 84 | 85 | {currencyList.map((currency) => ( 86 | handleCurrencySelect(currency)} 89 | > 90 | {currency.symbol} 91 | 92 | {currency.code} 93 | 94 | 101 | 102 | ))} 103 | 104 | 105 | 106 | 107 | 108 | 112 | onChange(e.target.value, selectedCurrency?.code || "USD") 113 | } 114 | className="w-full" 115 | placeholder="Price" 116 | /> 117 |
    118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { Cross2Icon } from "@radix-ui/react-icons"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const Dialog = DialogPrimitive.Root; 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger; 12 | 13 | const DialogPortal = DialogPrimitive.Portal; 14 | 15 | const DialogClose = DialogPrimitive.Close; 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )); 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )); 54 | DialogContent.displayName = DialogPrimitive.Content.displayName; 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
    67 | ); 68 | DialogHeader.displayName = "DialogHeader"; 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
    81 | ); 82 | DialogFooter.displayName = "DialogFooter"; 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )); 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )); 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogTrigger, 116 | DialogClose, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | }; 123 | -------------------------------------------------------------------------------- /src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Drawer as DrawerPrimitive } from "vaul"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ); 17 | Drawer.displayName = "Drawer"; 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger; 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal; 22 | 23 | const DrawerClose = DrawerPrimitive.Close; 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )); 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
    52 | {children} 53 | 54 | 55 | )); 56 | DrawerContent.displayName = "DrawerContent"; 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
    66 | ); 67 | DrawerHeader.displayName = "DrawerHeader"; 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
    77 | ); 78 | DrawerFooter.displayName = "DrawerFooter"; 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )); 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName; 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )); 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName; 106 | 107 | export { 108 | Drawer, 109 | DrawerPortal, 110 | DrawerOverlay, 111 | DrawerTrigger, 112 | DrawerClose, 113 | DrawerContent, 114 | DrawerHeader, 115 | DrawerFooter, 116 | DrawerTitle, 117 | DrawerDescription, 118 | }; 119 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Popover = PopoverPrimitive.Root; 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger; 11 | 12 | const PopoverAnchor = PopoverPrimitive.Anchor; 13 | 14 | const PopoverContent = React.forwardRef< 15 | React.ElementRef, 16 | React.ComponentPropsWithoutRef 17 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 18 | 19 | 29 | 30 | )); 31 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 32 | 33 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; 34 | -------------------------------------------------------------------------------- /src/components/ui/radio-group.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { CheckIcon } from "@radix-ui/react-icons"; 5 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const RadioGroup = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => { 13 | return ( 14 | 19 | ); 20 | }); 21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName; 22 | 23 | const RadioGroupItem = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => { 27 | return ( 28 | 36 | 37 | 38 | 39 | 40 | ); 41 | }); 42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName; 43 | 44 | export { RadioGroup, RadioGroupItem }; 45 | -------------------------------------------------------------------------------- /src/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const ScrollArea = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, children, ...props }, ref) => ( 12 | 17 | 18 | {children} 19 | 20 | 21 | 22 | 23 | )); 24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 25 | 26 | const ScrollBar = React.forwardRef< 27 | React.ElementRef, 28 | React.ComponentPropsWithoutRef 29 | >(({ className, orientation = "vertical", ...props }, ref) => ( 30 | 43 | 44 | 45 | )); 46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 47 | 48 | export { ScrollArea, ScrollBar }; 49 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref, 15 | ) => ( 16 | 27 | ), 28 | ); 29 | Separator.displayName = SeparatorPrimitive.Root.displayName; 30 | 31 | export { Separator }; 32 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
    12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as SwitchPrimitives from "@radix-ui/react-switch"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Switch = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | 25 | 26 | )); 27 | Switch.displayName = SwitchPrimitives.Root.displayName; 28 | 29 | export { Switch }; 30 | -------------------------------------------------------------------------------- /src/components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TabsPrimitive from "@radix-ui/react-tabs"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | const Tabs = TabsPrimitive.Root; 9 | 10 | const TabsList = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | TabsList.displayName = TabsPrimitive.List.displayName; 24 | 25 | const TabsTrigger = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 37 | )); 38 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; 39 | 40 | const TabsContent = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 | 52 | )); 53 | TabsContent.displayName = TabsPrimitive.Content.displayName; 54 | 55 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 56 | -------------------------------------------------------------------------------- /src/hooks/use-media-query.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export function useMediaQuery(query: string): boolean { 4 | const [matches, setMatches] = useState(false); 5 | 6 | useEffect(() => { 7 | const media = window.matchMedia(query); 8 | if (media.matches !== matches) { 9 | setMatches(media.matches); 10 | } 11 | const listener = () => setMatches(media.matches); 12 | media.addListener(listener); 13 | return () => media.removeListener(listener); 14 | }, [matches, query]); 15 | 16 | return matches; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/platforms.tsx: -------------------------------------------------------------------------------- 1 | import NetflixLogo from "@/components/logo/netflix"; 2 | import SpotifyLogo from "@/components/logo/spotify"; 3 | import YoutubeLogo from "@/components/logo/youtube"; 4 | import AppleLogo from "@/components/logo/apple"; 5 | import GameLogo from "@/components/logo/game"; 6 | import AdobeLogo from "@/components/logo/adobe"; 7 | import FigmaLogo from "@/components/logo/figma"; 8 | import CanvaLogo from "@/components/logo/canva"; 9 | import NotionLogo from "@/components/logo/notion"; 10 | import HuluLogo from "@/components/logo/hulu"; 11 | import AdobeXdLogo from "@/components/logo/adobe-xd"; 12 | import GodaddyLogo from "@/components/logo/godaddy"; 13 | import OpenaiLogo from "@/components/logo/openai"; 14 | import UdacityLogo from "@/components/logo/udacity"; 15 | import AWSLogo from "@/components/logo/aws"; 16 | import PerplexityLogo from "@/components/logo/perplexity"; 17 | import UdemyLogo from "@/components/logo/udemy"; 18 | import VercelLogo from "@/components/logo/vercel"; 19 | import WebflowLogo from "@/components/logo/webflow"; 20 | import DisneyPlusLogo from "@/components/logo/disney-plus"; 21 | import LinkedInLogo from "@/components/logo/linkedin"; 22 | import TwitchLogo from "@/components/logo/twitch"; 23 | import KickLogo from "@/components/logo/kick"; 24 | import PremiereLogo from "@/components/logo/premiere"; 25 | import StabilityAiLogo from "@/components/logo/stability-ai"; 26 | import FirebaseLogo from "@/components/logo/firebase"; 27 | import MongoDBLogo from "@/components/logo/mongodb"; 28 | import IllustratorLogo from "@/components/logo/illustrator"; 29 | import PhotoshopLogo from "@/components/logo/photoshop"; 30 | import LightRoomLogo from "@/components/logo/light-room"; 31 | import GamePlatformsLogo from "@/components/logo/game"; // Example for games 32 | import { CircleArrowOutUpRight } from "lucide-react"; 33 | 34 | const platformOptions = [ 35 | { 36 | value: "netflix", 37 | label: "Netflix", 38 | icon: NetflixLogo, 39 | category: "Entertainment", 40 | }, 41 | { 42 | value: "spotify", 43 | label: "Spotify", 44 | icon: SpotifyLogo, 45 | category: "Entertainment", 46 | }, 47 | { 48 | value: "youtube", 49 | label: "YouTube", 50 | icon: YoutubeLogo, 51 | category: "Entertainment", 52 | }, 53 | { 54 | value: "apple", 55 | label: "Apple", 56 | icon: AppleLogo, 57 | category: "Entertainment", 58 | }, 59 | { value: "games", label: "Games", icon: GameLogo, category: "Entertainment" }, 60 | { value: "adobe", label: "Adobe", icon: AdobeLogo, category: "Design" }, 61 | { 62 | value: "adobe-xd", 63 | label: "Adobe XD", 64 | icon: AdobeXdLogo, 65 | category: "Design", 66 | }, 67 | { value: "figma", label: "Figma", icon: FigmaLogo, category: "Design" }, 68 | { value: "canva", label: "Canva", icon: CanvaLogo, category: "Design" }, 69 | { 70 | value: "notion", 71 | label: "Notion", 72 | icon: NotionLogo, 73 | category: "Productivity", 74 | }, 75 | { value: "hulu", label: "Hulu", icon: HuluLogo, category: "Entertainment" }, 76 | { 77 | value: "godaddy", 78 | label: "GoDaddy", 79 | icon: GodaddyLogo, 80 | category: "Hosting", 81 | }, 82 | { value: "openai", label: "OpenAI", icon: OpenaiLogo, category: "AI" }, 83 | { 84 | value: "udacity", 85 | label: "Udacity", 86 | icon: UdacityLogo, 87 | category: "Education", 88 | }, 89 | { value: "aws", label: "AWS", icon: AWSLogo, category: "Hosting" }, 90 | { 91 | value: "perplexity", 92 | label: "Perplexity", 93 | icon: PerplexityLogo, 94 | category: "AI", 95 | }, 96 | { value: "udemy", label: "Udemy", icon: UdemyLogo, category: "Education" }, 97 | { value: "vercel", label: "Vercel", icon: VercelLogo, category: "Hosting" }, 98 | { value: "webflow", label: "Webflow", icon: WebflowLogo, category: "Design" }, 99 | { 100 | value: "disney-plus", 101 | label: "Disney+", 102 | icon: DisneyPlusLogo, 103 | category: "Entertainment", 104 | }, 105 | { 106 | value: "linkedin", 107 | label: "LinkedIn", 108 | icon: LinkedInLogo, 109 | category: "Social", 110 | }, 111 | { 112 | value: "twitch", 113 | label: "Twitch", 114 | icon: TwitchLogo, 115 | category: "Entertainment", 116 | }, 117 | { value: "kick", label: "Kick", icon: KickLogo, category: "Entertainment" }, 118 | { 119 | value: "premiere", 120 | label: "Premiere Pro", 121 | icon: PremiereLogo, 122 | category: "Design", 123 | }, 124 | { 125 | value: "stability-ai", 126 | label: "Stability AI", 127 | icon: StabilityAiLogo, 128 | category: "AI", 129 | }, 130 | { 131 | value: "firebase", 132 | label: "Firebase", 133 | icon: FirebaseLogo, 134 | category: "Hosting", 135 | }, 136 | { 137 | value: "mongodb", 138 | label: "MongoDB", 139 | icon: MongoDBLogo, 140 | category: "Database", 141 | }, 142 | { 143 | value: "illustrator", 144 | label: "Illustrator", 145 | icon: IllustratorLogo, 146 | category: "Design", 147 | }, 148 | { 149 | value: "photoshop", 150 | label: "Photoshop", 151 | icon: PhotoshopLogo, 152 | category: "Design", 153 | }, 154 | { 155 | value: "light-room", 156 | label: "Light Room", 157 | icon: LightRoomLogo, 158 | category: "Design", 159 | }, 160 | { 161 | value: "other", 162 | label: "Other", 163 | icon: CircleArrowOutUpRight, 164 | category: "Other", 165 | }, 166 | ]; 167 | 168 | export default platformOptions; 169 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/server/auth"; 2 | import { NextResponse } from "next/server"; 3 | 4 | // Define arrays for different types of routes 5 | const authRoutes = ["/login"]; 6 | const publicRoutes = ["/"]; 7 | 8 | export default auth((req) => { 9 | const { nextUrl, auth: session } = req; 10 | console.log(session, "middleware"); 11 | const isApiRoute = nextUrl.pathname.startsWith("/api"); 12 | const isAuthRoute = authRoutes.includes(nextUrl.pathname); 13 | const isPublicRoute = publicRoutes.includes(nextUrl.pathname); 14 | 15 | // Allow API routes to pass through 16 | if (isApiRoute) { 17 | return NextResponse.next(); 18 | } 19 | 20 | // Redirect logged-in users away from auth routes 21 | if (session && isAuthRoute) { 22 | return NextResponse.redirect(new URL("/dashboard", nextUrl.origin)); 23 | } 24 | 25 | // Allow access to public routes regardless of auth status 26 | if (isPublicRoute) { 27 | return NextResponse.next(); 28 | } 29 | 30 | // Redirect non-authenticated users to sign-in for private routes 31 | if (!session && !isAuthRoute) { 32 | const signInUrl = new URL("/login", nextUrl.origin); 33 | signInUrl.searchParams.set("redirectTo", nextUrl.pathname + nextUrl.search); 34 | return NextResponse.redirect(signInUrl); 35 | } 36 | 37 | // Allow access to all other routes for authenticated users 38 | return NextResponse.next(); 39 | }); 40 | 41 | export const config = { 42 | matcher: [ 43 | // Skip Next.js internals and all static files, unless found in search params 44 | "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)", 45 | ], 46 | }; 47 | -------------------------------------------------------------------------------- /src/server/actions/auth-actions.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { signIn, signOut } from "../auth"; 4 | 5 | export async function signInWithGoogle() { 6 | await signIn("google", { 7 | redirectTo: "/dashboard", 8 | }); 9 | } 10 | 11 | export async function signOutAction() { 12 | await signOut(); 13 | } 14 | -------------------------------------------------------------------------------- /src/server/auth.ts: -------------------------------------------------------------------------------- 1 | import { DrizzleAdapter } from "@auth/drizzle-adapter"; 2 | import { db, user, account, session, verificationTokens } from "./db"; 3 | import Google from "@auth/core/providers/google"; 4 | import NextAuth from "next-auth"; 5 | import type { NextAuthConfig } from "next-auth"; 6 | import { eq } from "drizzle-orm"; 7 | export const { 8 | handlers: { GET, POST }, 9 | signIn, 10 | signOut, 11 | auth, 12 | } = NextAuth({ 13 | secret: process.env.AUTH_SECRET, 14 | adapter: DrizzleAdapter(db, { 15 | usersTable: user, 16 | accountsTable: account, 17 | sessionsTable: session, 18 | verificationTokensTable: verificationTokens, 19 | }), 20 | pages: { 21 | signIn: "/login", 22 | }, 23 | events: { 24 | async linkAccount({ user: AdapterUser }) { 25 | await db 26 | .update(user) 27 | .set({ 28 | emailVerified: new Date(), 29 | }) 30 | .where(eq(user.id, AdapterUser.id as string)); 31 | }, 32 | }, 33 | callbacks: { 34 | async session({ session, user }) { 35 | const accounts = await db 36 | .select() 37 | .from(account) 38 | .where(eq(account.userId, user.id)); 39 | return { 40 | ...session, 41 | user: { 42 | ...session.user, 43 | accessToken: accounts[0].access_token, 44 | refreshToken: accounts[0].refresh_token, 45 | }, 46 | }; 47 | }, 48 | }, 49 | providers: [ 50 | Google({ 51 | clientId: process.env.AUTH_GOOGLE_ID, 52 | clientSecret: process.env.AUTH_GOOGLE_SECRET, 53 | // authorization: { 54 | // params: { 55 | // scope: 56 | // "openid email profile https://www.googleapis.com/auth/calendar", 57 | // }, 58 | // }, 59 | }), 60 | ], 61 | } as NextAuthConfig); 62 | -------------------------------------------------------------------------------- /src/server/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/d1"; 2 | 3 | import * as schema from "./schema"; 4 | export { user, account, session, verificationTokens } from "./schema"; 5 | 6 | export const db = drizzle(process.env.DB, { schema, logger: true }); 7 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | backgroundImage: { 13 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 14 | "gradient-conic": 15 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 16 | }, 17 | borderRadius: { 18 | lg: "var(--radius)", 19 | md: "calc(var(--radius) - 2px)", 20 | sm: "calc(var(--radius) - 4px)", 21 | }, 22 | colors: { 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | card: { 26 | DEFAULT: "hsl(var(--card))", 27 | foreground: "hsl(var(--card-foreground))", 28 | }, 29 | popover: { 30 | DEFAULT: "hsl(var(--popover))", 31 | foreground: "hsl(var(--popover-foreground))", 32 | }, 33 | primary: { 34 | DEFAULT: "hsl(var(--primary))", 35 | foreground: "hsl(var(--primary-foreground))", 36 | }, 37 | secondary: { 38 | DEFAULT: "hsl(var(--secondary))", 39 | foreground: "hsl(var(--secondary-foreground))", 40 | }, 41 | muted: { 42 | DEFAULT: "hsl(var(--muted))", 43 | foreground: "hsl(var(--muted-foreground))", 44 | }, 45 | accent: { 46 | DEFAULT: "hsl(var(--accent))", 47 | foreground: "hsl(var(--accent-foreground))", 48 | }, 49 | destructive: { 50 | DEFAULT: "hsl(var(--destructive))", 51 | foreground: "hsl(var(--destructive-foreground))", 52 | }, 53 | border: "hsl(var(--border))", 54 | input: "hsl(var(--input))", 55 | ring: "hsl(var(--ring))", 56 | chart: { 57 | "1": "hsl(var(--chart-1))", 58 | "2": "hsl(var(--chart-2))", 59 | "3": "hsl(var(--chart-3))", 60 | "4": "hsl(var(--chart-4))", 61 | "5": "hsl(var(--chart-5))", 62 | }, 63 | }, 64 | }, 65 | }, 66 | plugins: [require("tailwindcss-animate")], 67 | }; 68 | export default config; 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./src/*"] 22 | }, 23 | "types": ["@cloudflare/workers-types/2023-07-01"] 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | #:schema node_modules/wrangler/config-schema.json 2 | name = "resubs" 3 | compatibility_date = "2024-09-19" 4 | compatibility_flags = ["nodejs_compat"] 5 | pages_build_output_dir = ".vercel/output/static" 6 | 7 | # Automatically place your workloads in an optimal location to minimize latency. 8 | # If you are running back-end logic in a Pages Function, running it closer to your back-end infrastructure 9 | # rather than the end user may result in better performance. 10 | # Docs: https://developers.cloudflare.com/pages/functions/smart-placement/#smart-placement 11 | # [placement] 12 | # mode = "smart" 13 | 14 | # Variable bindings. These are arbitrary, plaintext strings (similar to environment variables) 15 | # Docs: 16 | # - https://developers.cloudflare.com/pages/functions/bindings/#environment-variables 17 | # Note: Use secrets to store sensitive data. 18 | # - https://developers.cloudflare.com/pages/functions/bindings/#secrets 19 | # [vars] 20 | # MY_VARIABLE = "production_value" 21 | 22 | # Bind the Workers AI model catalog. Run machine learning models, powered by serverless GPUs, on Cloudflare’s global network 23 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#workers-ai 24 | # [ai] 25 | # binding = "AI" 26 | 27 | # Bind a D1 database. D1 is Cloudflare’s native serverless SQL database. 28 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#d1-databases 29 | [[d1_databases]] 30 | binding = "DB" # i.e. available in your Worker on env.DB 31 | database_name = "resubs" 32 | database_id = "4e863a73-c256-4bde-aed6-3cfd865f0ab1" 33 | migrations_dir = "drizzle" 34 | 35 | # Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model. 36 | # Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps. 37 | # Docs: https://developers.cloudflare.com/workers/runtime-apis/durable-objects 38 | # [[durable_objects.bindings]] 39 | # name = "MY_DURABLE_OBJECT" 40 | # class_name = "MyDurableObject" 41 | # script_name = 'my-durable-object' 42 | 43 | # Bind a KV Namespace. Use KV as persistent storage for small key-value pairs. 44 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#kv-namespaces 45 | # KV Example: 46 | # [[kv_namespaces]] 47 | # binding = "MY_KV_NAMESPACE" 48 | # id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 49 | 50 | # Bind a Queue producer. Use this binding to schedule an arbitrary task that may be processed later by a Queue consumer. 51 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#queue-producers 52 | # [[queues.producers]] 53 | # binding = "MY_QUEUE" 54 | # queue = "my-queue" 55 | 56 | # Bind an R2 Bucket. Use R2 to store arbitrarily large blobs of data, such as files. 57 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#r2-buckets 58 | # [[r2_buckets]] 59 | # binding = "MY_BUCKET" 60 | # bucket_name = "my-bucket" 61 | 62 | # Bind another Worker service. Use this binding to call another Worker without network overhead. 63 | # Docs: https://developers.cloudflare.com/pages/functions/bindings/#service-bindings 64 | # [[services]] 65 | # binding = "MY_SERVICE" 66 | # service = "my-service" 67 | 68 | # To use different bindings for preview and production environments, follow the examples below. 69 | # When using environment-specific overrides for bindings, ALL bindings must be specified on a per-environment basis. 70 | # Docs: https://developers.cloudflare.com/pages/functions/wrangler-configuration#environment-specific-overrides 71 | 72 | ######## PREVIEW environment config ######## 73 | 74 | # [env.preview.vars] 75 | # API_KEY = "xyz789" 76 | 77 | # [[env.preview.kv_namespaces]] 78 | # binding = "MY_KV_NAMESPACE" 79 | # id = "" 80 | 81 | ######## PRODUCTION environment config ######## 82 | 83 | # [env.production.vars] 84 | # API_KEY = "abc123" 85 | 86 | # [[env.production.kv_namespaces]] 87 | # binding = "MY_KV_NAMESPACE" 88 | # id = "" 89 | --------------------------------------------------------------------------------