├── .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 |
7 |
--------------------------------------------------------------------------------
/public/logos/apple_tv.svg:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/public/logos/chatGPT.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/logos/hulu.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/logos/netflix.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/public/logos/spotify.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/public/logos/youtube.svg:
--------------------------------------------------------------------------------
1 |
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 |
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 |
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 |
15 |
16 | 404
17 |
18 |
19 |
This page could not be found.
20 |
21 |
22 |
23 | >
24 | );
25 | }
26 |
27 | const styles = {
28 | error: {
29 | fontFamily:
30 | 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
31 | height: "100vh",
32 | textAlign: "center",
33 | display: "flex",
34 | flexDirection: "column",
35 | alignItems: "center",
36 | justifyContent: "center",
37 | },
38 |
39 | desc: {
40 | display: "inline-block",
41 | },
42 |
43 | h1: {
44 | display: "inline-block",
45 | margin: "0 20px 0 0",
46 | padding: "0 23px 0 0",
47 | fontSize: 24,
48 | fontWeight: 500,
49 | verticalAlign: "top",
50 | lineHeight: "49px",
51 | },
52 |
53 | h2: {
54 | fontSize: 14,
55 | fontWeight: 400,
56 | lineHeight: "49px",
57 | margin: 0,
58 | },
59 | } as const;
60 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useRef, useState } from "react";
4 | import { motion, useAnimation } from "framer-motion";
5 | import Image from "next/image";
6 | import Link from "next/link";
7 | import { Button } from "@/components/ui/button";
8 |
9 | const logos = [
10 | { src: "/logos/netflix.svg", alt: "Netflix" },
11 | { src: "/logos/apple_tv.svg", alt: "apple_tv" },
12 | { src: "/logos/spotify.svg", alt: "spotify" },
13 | { src: "/logos/youtube.svg", alt: "youtube" },
14 | { src: "/logos/chatGPT.svg", alt: "chatGPT" },
15 | { src: "/logos/hulu.svg", alt: "hulu" },
16 | ];
17 |
18 | const cursorColors = [
19 | "#E50914", // Netflix red
20 | "#000000", // Apple TV black
21 | "#1DB954", // Spotify green
22 | "#FF0000", // YouTube red
23 | "#10A37F", // ChatGPT green
24 | "#38E783", // Hulu green
25 | ];
26 |
27 | const logoPositions = [
28 | { x: 10, y: 5 },
29 | { x: 15, y: 20 },
30 | { x: 70, y: 20 },
31 | { x: 80, y: 25 },
32 | { x: 30, y: 25 },
33 | { x: 80, y: 5 },
34 | ];
35 |
36 | export default function HeroSection() {
37 | const containerRef = useRef(null);
38 | // biome-ignore lint/correctness/useHookAtTopLevel:
39 | const cursorControls = logos.map(() => useAnimation());
40 | // biome-ignore lint/correctness/useHookAtTopLevel:
41 | const logoControls = logos.map(() => useAnimation());
42 | const [animationComplete, setAnimationComplete] = useState(false);
43 |
44 | useEffect(() => {
45 | const animateElements = async () => {
46 | for (let i = 0; i < logos.length; i++) {
47 | const startX = -10;
48 | const startY = logoPositions[i].y;
49 | const endX = logoPositions[i].x;
50 | const endY = logoPositions[i].y;
51 |
52 | await Promise.all([
53 | cursorControls[i].start({
54 | left: [`${startX}%`, `${endX}%`, "110%"],
55 | top: [`${startY}%`, `${endY}%`, `${endY}%`],
56 | opacity: [0, 1, 0],
57 | transition: { duration: 2, times: [0, 0.5, 1] },
58 | }),
59 | logoControls[i].start({
60 | left: [`${startX}%`, `${endX}%`, `${endX}%`],
61 | top: [`${startY}%`, `${endY}%`, `${endY}%`],
62 | opacity: [0, 1, 1],
63 | scale: [0, 1, 1],
64 | transition: { duration: 2, times: [0, 0.5, 1] },
65 | }),
66 | ]);
67 | }
68 | };
69 | setAnimationComplete(true);
70 | animateElements();
71 | }, []);
72 |
73 | return (
74 |
78 |
79 | {logos.map((logo, index) => (
80 |
86 |
99 |
100 | ))}
101 |
102 | {logos.map((logo, index) => (
103 |
109 |
125 |
126 | ))}
127 |
128 |
129 |
130 | Are you Tired of Tracking Your Subscriptions?
131 |
132 |
133 | yeah, we thought so.
134 |
135 |
136 |
141 |
142 |
143 |
144 |
151 |
152 |
153 | );
154 | }
155 |
--------------------------------------------------------------------------------
/src/components/analytics/graphs/analytics.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React from "react";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardHeader,
9 | CardTitle,
10 | } from "@/components/ui/card";
11 | import { PieChart, type PieChartItem } from "./pichart";
12 | import { ScrollArea } from "@/components/ui/scroll-area";
13 |
14 | // Dummy subscription data
15 | const subscriptions = [
16 | {
17 | id: 1,
18 | name: "Netflix",
19 | price: 15.99,
20 | platform: "Streaming",
21 | nextPayment: "2023-07-15",
22 | },
23 | {
24 | id: 2,
25 | name: "Spotify",
26 | price: 9.99,
27 | platform: "Music",
28 | nextPayment: "2023-07-20",
29 | },
30 | {
31 | id: 3,
32 | name: "Amazon Prime",
33 | price: 12.99,
34 | platform: "Shopping",
35 | nextPayment: "2023-07-05",
36 | },
37 | {
38 | id: 4,
39 | name: "Disney+",
40 | price: 7.99,
41 | platform: "Streaming",
42 | nextPayment: "2023-07-18",
43 | },
44 | {
45 | id: 5,
46 | name: "GitHub",
47 | price: 4.99,
48 | platform: "Development",
49 | nextPayment: "2023-07-10",
50 | },
51 | ];
52 |
53 | // Calculate total spending by platform
54 | const spendingByPlatform = subscriptions.reduce(
55 | (acc, sub) => {
56 | acc[sub.platform] = (acc[sub.platform] || 0) + sub.price;
57 | return acc;
58 | },
59 | {} as Record,
60 | );
61 |
62 | // Prepare data for pie chart
63 | const pieChartData: PieChartItem[] = Object.entries(spendingByPlatform).map(
64 | ([name, value]) => ({
65 | name,
66 | value,
67 | }),
68 | );
69 |
70 | // Sort upcoming payments
71 | const upcomingPayments = [...subscriptions].sort(
72 | (a, b) =>
73 | new Date(a.nextPayment).getTime() - new Date(b.nextPayment).getTime(),
74 | );
75 |
76 | export function SubscriptionAnalytics() {
77 | return (
78 |
79 |
80 |
81 | Monthly Spending by Platform
82 |
83 | Distribution of your subscription costs across different platforms
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | Upcoming Payments
93 |
94 | Your subscription payments due this month
95 |
96 |
97 |
98 |
99 |
100 | {upcomingPayments.map((sub) => (
101 |
102 |
103 |
{sub.name}
104 |
105 | {sub.platform}
106 |
107 |
108 |
109 |
${sub.price.toFixed(2)}
110 |
111 | {new Date(sub.nextPayment).toLocaleDateString()}
112 |
113 |
114 |
115 | ))}
116 |
117 |
118 |
119 |
120 |
121 | );
122 | }
123 |
--------------------------------------------------------------------------------
/src/components/analytics/graphs/pichart.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | PieChart as RechartsPieChart,
4 | Pie,
5 | Cell,
6 | Legend,
7 | Tooltip,
8 | } from "recharts";
9 |
10 | export interface PieChartItem {
11 | name: string;
12 | value: number;
13 | }
14 |
15 | interface PieChartProps {
16 | data: PieChartItem[];
17 | }
18 |
19 | const COLORS = ["#0ea5e9", "#0284c7", "#0369a1", "#075985", "#0c4a6e"];
20 |
21 | export function PieChart({ data }: PieChartProps) {
22 | return (
23 |
24 |
33 | {data.map((entry, index) => (
34 | |
38 | ))}
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/analytics/subscription-analytics.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import React, { useState, useEffect } from "react";
4 | import { format, addMonths, subMonths } from "date-fns";
5 | // import { TotalMonthlyExpenditure } from "./graphs/total-monthly-expenditure";
6 | // import { SpendingBreakdownByPlatform } from "./graphs/spending-breakdown-by-platform";
7 | // import { MonthlySpendingByPlatform } from "./graphs/monthly-spending-by-platform";
8 | // import { AverageSubscriptionPrice } from "./graphs/average-subscription-price";
9 | // import { UpcomingSubscriptionRenewals } from "./graphs/upcoming-subscription-renewals";
10 | // import { LifetimeValueOfSubscriptions } from "./graphs/lifetime-value-of-subscriptions";
11 | import { Button } from "@/components/ui/button";
12 | import { ChevronLeft, ChevronRight } from "lucide-react";
13 | import type { subscriptionSelectType } from "@/server/db/schema";
14 | import { useQuery } from "@tanstack/react-query";
15 | import { getAllSubscriptions } from "@/server/actions/subscriptions";
16 |
17 | export function SubscriptionAnalytics() {
18 | const [currentDate, setCurrentDate] = useState(new Date());
19 |
20 | const { data: subscriptions } = useQuery({
21 | queryKey: ["subscriptions"],
22 | queryFn: () => getAllSubscriptions(),
23 | });
24 |
25 | const handlePreviousMonth = () => {
26 | setCurrentDate((prevDate) => subMonths(prevDate, 1));
27 | };
28 |
29 | const handleNextMonth = () => {
30 | setCurrentDate((prevDate) => addMonths(prevDate, 1));
31 | };
32 |
33 | if (!subscriptions) return Loading...
;
34 |
35 | return (
36 |
37 |
38 |
Subscription Analytics
39 |
40 |
43 |
44 | {format(currentDate, "MMMM yyyy")}
45 |
46 |
49 |
50 |
51 |
52 | {/*
56 |
60 |
64 |
65 |
69 |
*/}
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/calendar/calendar-header.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import {
3 | AnimatePresence,
4 | motion,
5 | useSpring,
6 | useTransform,
7 | } from "framer-motion";
8 | import { ChevronLeft, ChevronRight } from "lucide-react";
9 | import { cn } from "@/lib/utils"; // Adjust your import for cn utility if necessary
10 | import { useEffect } from "react";
11 | import { addTestSubscription } from "@/server/actions/subscriptions";
12 |
13 | type CalendarHeaderProps = {
14 | currentMonth: Date;
15 | onPrevMonth: () => void;
16 | onNextMonth: () => void;
17 | slideDirection: "up" | "down";
18 | totalMoneySpent: number;
19 | onTotalMoneyClick: () => void;
20 | };
21 |
22 | export function CalendarHeader({
23 | currentMonth,
24 | onPrevMonth,
25 | onNextMonth,
26 | slideDirection,
27 | totalMoneySpent,
28 | onTotalMoneyClick,
29 | }: CalendarHeaderProps) {
30 | const monthSpring = useSpring(currentMonth.getMonth(), {
31 | stiffness: 100,
32 | damping: 20,
33 | });
34 | const yearSpring = useSpring(currentMonth.getFullYear(), {
35 | stiffness: 100,
36 | damping: 20,
37 | });
38 |
39 | const totalMoneySpentSpring = useSpring(totalMoneySpent, {
40 | stiffness: 100,
41 | damping: 20,
42 | });
43 |
44 | const monthDisplay = useTransform(monthSpring, (current) =>
45 | new Date(0, Math.round(current)).toLocaleString("default", {
46 | month: "long",
47 | }),
48 | );
49 | const yearDisplay = useTransform(yearSpring, (current) =>
50 | Math.round(current).toString(),
51 | );
52 |
53 | const totalMoneySpentDisplay = useTransform(
54 | totalMoneySpentSpring,
55 | (current) => current.toFixed(2),
56 | );
57 |
58 | useEffect(() => {
59 | monthSpring.set(currentMonth.getMonth());
60 | yearSpring.set(currentMonth.getFullYear());
61 | totalMoneySpentSpring.set(totalMoneySpent);
62 | }, [
63 | currentMonth,
64 | monthSpring,
65 | yearSpring,
66 | totalMoneySpent,
67 | totalMoneySpentSpring,
68 | ]);
69 |
70 | return (
71 |
72 |
73 |
76 |
79 |
80 |
81 | {monthDisplay}
82 | {" "}
83 |
84 | {yearDisplay}
85 |
86 |
87 |
88 |
98 |
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/src/components/dashboard/expense-breakdown.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { TrendingUp } from "lucide-react";
5 | import { Label, Pie, PieChart } from "recharts";
6 | import { Apple, Youtube, Music, Tv, Gamepad } from "lucide-react";
7 |
8 | import {
9 | Card,
10 | CardContent,
11 | CardDescription,
12 | CardFooter,
13 | CardHeader,
14 | CardTitle,
15 | } from "@/components/ui/card";
16 | import {
17 | type ChartConfig,
18 | ChartContainer,
19 | ChartTooltip,
20 | ChartTooltipContent,
21 | } from "@/components/ui/chart";
22 |
23 | export const description = "A donut chart showing subscription expenses";
24 |
25 | const chartData = [
26 | { platform: "netflix", expense: 15.99, fill: "var(--color-netflix)" },
27 | { platform: "spotify", expense: 9.99, fill: "var(--color-spotify)" },
28 | { platform: "youtube", expense: 11.99, fill: "var(--color-youtube)" },
29 | { platform: "apple", expense: 14.95, fill: "var(--color-apple)" },
30 | { platform: "games", expense: 14.99, fill: "var(--color-games)" },
31 | ];
32 |
33 | const chartConfig = {
34 | // expense: {
35 | // label: "Expense",
36 | // },
37 | netflix: {
38 | label: "Netflix",
39 | color: "hsl(var(--chart-1))",
40 | icon: Tv,
41 | },
42 | spotify: {
43 | label: "Spotify",
44 | color: "hsl(var(--chart-2))",
45 | icon: Music,
46 | },
47 | youtube: {
48 | label: "YouTube",
49 | color: "hsl(var(--chart-3))",
50 | icon: Youtube,
51 | },
52 | apple: {
53 | label: "Apple",
54 | color: "hsl(var(--chart-4))",
55 | icon: Apple,
56 | },
57 | games: {
58 | label: "Games",
59 | color: "hsl(var(--chart-5))",
60 | icon: Gamepad,
61 | },
62 | } satisfies ChartConfig;
63 |
64 | export default function Component() {
65 | const totalExpense = React.useMemo(() => {
66 | return chartData.reduce((acc, curr) => acc + curr.expense, 0);
67 | }, []);
68 |
69 | return (
70 |
71 |
72 | Subscription Expenses
73 | Monthly Breakdown
74 |
75 |
76 |
80 |
81 | }
84 | />
85 |
94 |
124 | {chartData.map((entry, index) => {
125 | const Icon =
126 | chartConfig[entry.platform as keyof typeof chartConfig]?.icon;
127 | const angle = index * (360 / chartData.length) + 180;
128 | const radius = 100;
129 | const x = Math.cos((angle * Math.PI) / 180) * radius + 125;
130 | const y = Math.sin((angle * Math.PI) / 180) * radius + 125;
131 | return (
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | );
140 | })}
141 |
142 |
143 |
144 |
145 |
146 | Trending up by 3.2% this month
147 |
148 |
149 | Showing total subscription expenses for this month
150 |
151 |
152 |
153 | );
154 | }
155 |
--------------------------------------------------------------------------------
/src/components/dashboard/signout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button } from "../ui/button";
3 | import { signOut } from "@/server/auth";
4 |
5 | export const runtime = "edge";
6 |
7 | export default function SignOut() {
8 | return (
9 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/layout/provider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4 |
5 | export default function QueryProvider({
6 | children,
7 | }: { children: React.ReactNode }) {
8 | const queryClient = new QueryClient();
9 | return (
10 | {children}
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/src/components/layout/themeprovider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ThemeProvider as NextThemesProvider } from "next-themes";
5 | import type { ThemeProviderProps } from "next-themes/dist/types";
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children};
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/logo/adobe-animate.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const Animate = (props: SVGProps) => (
4 |
39 | );
40 | export default Animate;
41 |
--------------------------------------------------------------------------------
/src/components/logo/adobe-xd.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const XD = (props: SVGProps) => (
4 |
19 | );
20 | export default XD;
21 |
--------------------------------------------------------------------------------
/src/components/logo/adobe.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const Adobe = (props: SVGProps) => (
4 |
24 | );
25 | export default Adobe;
26 |
--------------------------------------------------------------------------------
/src/components/logo/after-effects.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const AfterEffects = (props: SVGProps) => (
4 |
30 | );
31 | export default AfterEffects;
32 |
--------------------------------------------------------------------------------
/src/components/logo/apple.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const Apple = (props: SVGProps) => (
4 |
13 | );
14 | export default Apple;
15 |
--------------------------------------------------------------------------------
/src/components/logo/aws.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import type { SVGProps } from "react";
3 | const AmazonWebServices = (props: SVGProps) => (
4 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------