diff --git a/.prettierignore b/.prettierignore index 5c065636..1efa0e5b 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,4 +4,5 @@ node_modules next-env.d.ts .husky bun.lockb -*.toml \ No newline at end of file +*.toml +drizzle \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index 07bda34c..d0325d3e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/cspell.config.yaml b/cspell.config.yaml index b5e73418..2e5aaa3a 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -52,3 +52,4 @@ words: - typecheck - Uploadthing - Upstash + - healthcheck diff --git a/drizzle/0001_minor_warlock.sql b/drizzle/0001_minor_warlock.sql new file mode 100644 index 00000000..890edb9d --- /dev/null +++ b/drizzle/0001_minor_warlock.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS "modules" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "user_id" text NOT NULL, + "name" text NOT NULL, + "description" text NOT NULL, + "code" text NOT NULL, + "icon" text DEFAULT 'default' NOT NULL, + "color" text DEFAULT 'default' NOT NULL, + "archived" boolean DEFAULT false NOT NULL, + "credits" integer DEFAULT 0 NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "modified_at" timestamp DEFAULT now() NOT NULL, + "last_visited" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "modules_id_unique" UNIQUE("id") +); diff --git a/drizzle/0002_complex_sally_floyd.sql b/drizzle/0002_complex_sally_floyd.sql new file mode 100644 index 00000000..c9775c88 --- /dev/null +++ b/drizzle/0002_complex_sally_floyd.sql @@ -0,0 +1 @@ +ALTER TABLE "modules" ALTER COLUMN "description" DROP NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 55b95007..0c1823b4 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,7 +1,5 @@ { - "id": "b304d26a-1927-40c9-9067-57935d6d9ab3", - "prevId": "00000000-0000-0000-0000-000000000000", - "version": "6", + "version": "7", "dialect": "postgresql", "tables": { "public.early_access": { @@ -60,8 +58,10 @@ "uniqueConstraints": { "early_access_email_unique": { "name": "early_access_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] + "columns": [ + "email" + ], + "nullsNotDistinct": false } } } @@ -69,8 +69,10 @@ "enums": {}, "schemas": {}, "_meta": { - "columns": {}, "schemas": {}, - "tables": {} - } -} + "tables": {}, + "columns": {} + }, + "id": "b304d26a-1927-40c9-9067-57935d6d9ab3", + "prevId": "00000000-0000-0000-0000-000000000000" +} \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 00000000..c4fc9e1e --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,177 @@ +{ + "id": "e2f7138b-3bc5-4e5c-8e9f-df04364cddcb", + "prevId": "b304d26a-1927-40c9-9067-57935d6d9ab3", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.early_access": { + "name": "early_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "invitation_sent_at": { + "name": "invitation_sent_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "early_access_email_unique": { + "name": "early_access_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "public.modules": { + "name": "modules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "modified_at": { + "name": "modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_visited": { + "name": "last_visited", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "modules_id_unique": { + "name": "modules_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 00000000..f5704eed --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,177 @@ +{ + "id": "6f3e4fea-8b33-40d5-a9cd-1c4bbb28d564", + "prevId": "e2f7138b-3bc5-4e5c-8e9f-df04364cddcb", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.early_access": { + "name": "early_access", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved": { + "name": "approved", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "invitation_sent_at": { + "name": "invitation_sent_at", + "type": "timestamp (3)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "early_access_email_unique": { + "name": "early_access_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "public.modules": { + "name": "modules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "archived": { + "name": "archived", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "modified_at": { + "name": "modified_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_visited": { + "name": "last_visited", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "modules_id_unique": { + "name": "modules_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 1768128c..6f86fa2b 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,20 @@ "when": 1716742272276, "tag": "0000_funny_johnny_blaze", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1722099574439, + "tag": "0001_minor_warlock", + "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1724179669909, + "tag": "0002_complex_sally_floyd", + "breakpoints": true } ] -} +} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index 05fc4d8d..e6d2ae82 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -48,7 +48,6 @@ export default tseslint.config( // JSDoc plugin only for TypeScript files { files: ['**/*.{ts,tsx}'], - // @ts-expect-error - types are wrong extends: [jsdoc.configs['flat/recommended-typescript-error']], }, @@ -92,6 +91,8 @@ export default tseslint.config( { checksVoidReturn: { attributes: false } }, ], + '@typescript-eslint/non-nullable-type-assertion-style': 'off', + '@typescript-eslint/dot-notation': 'off', '@typescript-eslint/no-unnecessary-condition': [ diff --git a/eslint.d.ts b/eslint.d.ts index 9fef8b7f..4f7dadfb 100644 --- a/eslint.d.ts +++ b/eslint.d.ts @@ -33,8 +33,8 @@ declare module '@eslint/eslintrc' { resolvePluginsRelativeTo: string; }); - extends(extendsValue: string): Linter.FlatConfig & { - [Symbol.iterator]: () => IterableIterator; + extends(extendsValue: string): Linter.Config & { + [Symbol.iterator]: () => IterableIterator; }; } } @@ -44,7 +44,7 @@ declare module '@eslint/compat' { import type { ConfigWithExtends } from 'typescript-eslint'; export const fixupConfigRules: ( - config: string | Linter.FlatConfig, + config: string | Linter.Config, ) => ConfigWithExtends[]; } diff --git a/next.config.js b/next.config.js index 7f9377fb..798e5089 100644 --- a/next.config.js +++ b/next.config.js @@ -9,7 +9,7 @@ jiti('./src/env'); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, - transpilePackages: ['next-mdx-remote'], + transpilePackages: ['next-mdx-remote', 'lucide-react'], // We run ESLint and TypeScript separately in the CI pipeline eslint: { diff --git a/package.json b/package.json index d744ea68..6cf07c27 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ } }, "dependencies": { - "@clerk/nextjs": "^5.2.6", - "@clerk/themes": "^2.1.13", + "@clerk/nextjs": "^5.3.7", + "@clerk/themes": "^2.1.25", "@hookform/resolvers": "^3.9.0", "@neondatabase/serverless": "^0.9.4", "@paralleldrive/cuid2": "^2.2.2", @@ -64,85 +64,91 @@ "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-navigation-menu": "^1.2.0", + "@radix-ui/react-popover": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", - "@react-email/components": "^0.0.22", - "@t3-oss/env-nextjs": "^0.11.0", - "@tanstack/react-query": "^5.51.11", + "@react-email/components": "^0.0.24", + "@t3-oss/env-nextjs": "^0.11.1", + "@tanstack/react-query": "^5.53.1", "@trpc/client": "next", "@trpc/react-query": "next", "@trpc/server": "next", - "@upstash/redis": "^1.33.0", + "@upstash/redis": "^1.34.0", "@vercel/analytics": "^1.3.1", "@vercel/speed-insights": "^1.0.12", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", - "drizzle-orm": "^0.32.1", + "drizzle-orm": "^0.33.0", "drizzle-zod": "^0.5.1", + "framer-motion": "^11.3.31", "geist": "^1.3.1", "jiti": "^1.21.6", - "lucide-react": "^0.416.0", - "next": "14.2.5", + "lucide-react": "^0.437.0", + "next": "14.2.7", "next-mdx-remote": "^5.0.0", "next-themes": "^0.3.0", "react": "^18.3.1", + "react-animate-height": "^3.2.3", "react-dom": "^18.3.1", - "react-email": "^2.1.6", - "react-hook-form": "^7.52.1", - "resend": "^3.5.0", + "react-email": "^3.0.1", + "react-hook-form": "^7.53.0", + "resend": "^4.0.0", "server-only": "^0.0.1", "sonner": "^1.5.0", "superjson": "^2.2.1", - "tailwind-merge": "^2.4.0", + "tailwind-merge": "^2.5.2", + "use-resize-observer": "^9.1.0", "zod": "^3.23.8" }, "devDependencies": { "@changesets/cli": "^2.27.7", - "@commitlint/cli": "^19.3.0", - "@commitlint/config-conventional": "^19.2.2", - "@commitlint/cz-commitlint": "^19.2.0", - "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0", + "@commitlint/cli": "^19.4.1", + "@commitlint/config-conventional": "^19.4.1", + "@commitlint/cz-commitlint": "^19.4.0", + "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0", "@eslint/compat": "^1.1.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "^9.7.0", - "@happy-dom/global-registrator": "^14.12.3", + "@eslint/js": "^9.9.1", + "@happy-dom/global-registrator": "^15.7.2", "@ianvs/prettier-plugin-sort-imports": "^4.3.1", - "@next/eslint-plugin-next": "^14.2.5", + "@next/eslint-plugin-next": "^14.2.7", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/container-queries": "^0.1.1", - "@tailwindcss/typography": "^0.5.13", - "@total-typescript/ts-reset": "^0.5.1", - "@types/bun": "^1.1.6", - "@types/eslint": "^9.6.0", + "@tailwindcss/typography": "^0.5.15", + "@total-typescript/ts-reset": "^0.6.0", + "@types/bun": "^1.1.8", + "@types/eslint": "^9.6.1", "@types/eslint-config-prettier": "^6.11.3", - "@types/node": "^20.14.12", - "@types/react": "^18.3.3", + "@types/node": "^22.5.2", + "@types/react": "^18.3.5", "@types/react-dom": "^18.3.0", "commitizen": "^4.3.0", - "cspell": "^8.12.1", - "drizzle-kit": "^0.23.0", - "eslint": "^9.7.0", + "cspell": "^8.14.2", + "drizzle-kit": "^0.24.2", + "eslint": "^9.9.1", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-jsdoc": "^48.8.3", + "eslint-plugin-jsdoc": "^50.2.2", "eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-react": "^7.35.0", "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-regexp": "^2.6.0", "eslint-plugin-security": "^3.0.1", "eslint-plugin-tailwindcss": "^3.17.4", - "globals": "^15.8.0", - "husky": "^9.1.2", - "lint-staged": "^15.2.7", - "markdownlint": "^0.34.0", + "globals": "^15.9.0", + "husky": "^9.1.5", + "lint-staged": "^15.2.9", + "markdownlint": "^0.35.0", "markdownlint-cli": "^0.41.0", - "postcss": "^8.4.40", + "postcss": "^8.4.42", "prettier": "^3.3.3", "prettier-plugin-curly": "^0.2.2", - "prettier-plugin-packagejson": "^2.5.1", - "tailwindcss": "^3.4.7", + "prettier-plugin-packagejson": "^2.5.2", + "tailwindcss": "^3.4.10", "tailwindcss-animate": "^1.0.7", "typescript": "^5.5.4", - "typescript-eslint": "^8.0.0-alpha.20" + "typescript-eslint": "^8.3.0" }, "trustedDependencies": [ "@clerk/shared", diff --git a/src/app/(dashboard)/app/_components/create-module-popover.tsx b/src/app/(dashboard)/app/_components/create-module-popover.tsx new file mode 100644 index 00000000..e6d70245 --- /dev/null +++ b/src/app/(dashboard)/app/_components/create-module-popover.tsx @@ -0,0 +1,337 @@ +'use client'; + +import { Button } from '@/primitives/button'; +import { Input } from '@/primitives/input'; +import { Popover, PopoverContent, PopoverTrigger } from '@/primitives/popover'; +import { ResizablePanel } from '@/primitives/resizable-panel'; +import { Separator } from '@/primitives/separator'; +import { ChevronRightIcon, PlusIcon } from 'lucide-react'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useState } from 'react'; +import { z } from 'zod'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/primitives/form'; +import { Label } from '@/primitives/label'; +import type { IconNames } from '@/primitives/icon'; +import { Icon, iconNames } from '@/primitives/icon'; +import { grayDark } from '@radix-ui/colors'; +import { colorChoices } from '../lib/color-choices'; +import { ScrollArea } from '@/primitives/scroll-area'; +import { api } from '@/lib/trpc/react'; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; +import { Textarea } from '@/primitives/textarea'; + +interface StepHeadingProps { + title: string; + description: string; +} + +const StepHeading = ({ title, description }: StepHeadingProps) => { + return ( +
+
{title}
+
+ {description} +
+ +
+ ); +}; + +const formSchema = z.object({ + name: z.string().min(2), + description: z.string().optional(), + code: z.string().min(2), + credits: z.string(), + icon: z.string().min(1), + color: z.string().min(1), +}); + +export function CreateModulePopover() { + const [popoverOpen, setPopoverOpen] = useState(false); + const [step, setStep] = useState<1 | 2 | 3>(1); + + const router = useRouter(); + + const mutation = api.modules.create.useMutation({ + onError(error) { + toast.error(error.message); + }, + onSuccess() { + router.refresh(); + }, + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + name: '', + code: '', + credits: '0', + icon: 'default', + color: 'default', + }, + }); + + async function onSubmit(values: z.infer) { + await mutation.mutateAsync({ + ...values, + credits: parseInt(values.credits), + }); + form.reset({ + name: '', + code: '', + credits: '0', + icon: 'default', + color: 'default', + }); + } + + return ( + { + setPopoverOpen((prev) => !prev); + setStep(1); + }} + > + + + + + + +
+ + {step === 1 && ( +
+ +
+ ( + + + Name + + + + + + + )} + /> + ( + + + Description (optional) + + +