diff --git a/apps/backend/package.json b/apps/backend/package.json new file mode 100644 index 0000000..4943d39 --- /dev/null +++ b/apps/backend/package.json @@ -0,0 +1,12 @@ +{ + "name": "@workspace/backend", + "license": "MIT", + "type": "module", + "exports": { + "./supabase/types": "./types.ts", + "./supabase/utils": "./utils.ts" + }, + "scripts": { + "dev": "npx supabase start && npx supabase functions serve" + } +} diff --git a/apps/backend/supabase/config.toml b/apps/backend/supabase/config.toml index ccca261..2482f0a 100644 --- a/apps/backend/supabase/config.toml +++ b/apps/backend/supabase/config.toml @@ -7,7 +7,7 @@ project_id = "backend" [api] enabled = true # Port to use for the API URL. -port = 94321 +port = 14321 # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API # endpoints. `public` and `graphql_public` schemas are included by default. schemas = ["public", "graphql_public"] @@ -23,9 +23,9 @@ enabled = false [db] # Port to use for the local database URL. -port = 94322 +port = 14322 # Port used by db diff command to initialize the shadow database. -shadow_port = 94320 +shadow_port = 14320 # The database major version to use. This has to be the same as your remote database's. Run `SHOW # server_version;` on the remote database to check. major_version = 17 @@ -33,7 +33,7 @@ major_version = 17 [db.pooler] enabled = false # Port to use for the local connection pooler. -port = 94329 +port = 14329 # Specifies when a server connection can be reused by other clients. # Configure one of the supported pooler modes: `transaction`, `session`. pool_mode = "transaction" diff --git a/apps/backend/supabase/migrations/20251111143326_profiles_and_repositories.sql b/apps/backend/supabase/migrations/20251111143326_profiles_and_repositories.sql new file mode 100644 index 0000000..366f05b --- /dev/null +++ b/apps/backend/supabase/migrations/20251111143326_profiles_and_repositories.sql @@ -0,0 +1,77 @@ +create or replace function public.handle_new_user() +returns trigger as $$ +begin + insert into public.profiles (id, full_name) + values (new.id, new.raw_user_meta_data->>'full_name'); + return new; +end; +$$ language plpgsql security definer; + +create trigger on_auth_user_created +after insert on auth.users +for each row execute function public.handle_new_user(); + +create type "public"."profile_repositories_statuses" as enum ('PENDING', 'ACCEPTED'); + +create table "public"."profile_repositories" ( + "profile" uuid not null default gen_random_uuid(), + "repository" uuid not null default gen_random_uuid(), + "status" profile_repositories_statuses not null default 'PENDING'::profile_repositories_statuses, + "created_at" timestamp with time zone not null default now() +); + +create policy "Enable users to view their own data only" +on "public"."profile_repositories" +as permissive +for select +to authenticated +using ((( SELECT auth.uid() AS uid) = profile)); + +alter table "public"."profile_repositories" enable row level security; + +create table "public"."profiles" ( + "id" uuid not null default auth.uid(), + "full_name" text not null, + "created_at" timestamp with time zone not null default now() +); + +alter table "public"."profiles" enable row level security; + +create table "public"."repositories" ( + "id" uuid not null default gen_random_uuid(), + "name" text not null, + "created_at" timestamp with time zone not null default now() +); + +alter table "public"."repositories" enable row level security; + +CREATE UNIQUE INDEX profile_repositories_pkey ON public.profile_repositories USING btree (profile, repository); + +CREATE UNIQUE INDEX profiles_pkey ON public.profiles USING btree (id); + +CREATE UNIQUE INDEX repositories_pkey ON public.repositories USING btree (id); + +alter table "public"."profile_repositories" add constraint "profile_repositories_pkey" PRIMARY KEY using index "profile_repositories_pkey"; + +alter table "public"."profiles" add constraint "profiles_pkey" PRIMARY KEY using index "profiles_pkey"; + +alter table "public"."repositories" add constraint "repositories_pkey" PRIMARY KEY using index "repositories_pkey"; + +alter table "public"."profile_repositories" add constraint "profile_repositories_profile_fkey" FOREIGN KEY (profile) REFERENCES profiles(id) not valid; + +alter table "public"."profile_repositories" validate constraint "profile_repositories_profile_fkey"; + +alter table "public"."profile_repositories" add constraint "profile_repositories_repository_fkey" FOREIGN KEY (repository) REFERENCES repositories(id) not valid; + +alter table "public"."profile_repositories" validate constraint "profile_repositories_repository_fkey"; + +alter table "public"."profiles" add constraint "profiles_id_fkey" FOREIGN KEY (id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE SET DEFAULT not valid; + +alter table "public"."profiles" validate constraint "profiles_id_fkey"; + +create policy "Enable write for all auth" +on "public"."repositories" +as permissive +for insert +to authenticated +with check (true); \ No newline at end of file diff --git a/apps/backend/supabase/migrations/20251111160101_create_repository_function.sql b/apps/backend/supabase/migrations/20251111160101_create_repository_function.sql new file mode 100644 index 0000000..6a86bc2 --- /dev/null +++ b/apps/backend/supabase/migrations/20251111160101_create_repository_function.sql @@ -0,0 +1,25 @@ +CREATE OR REPLACE FUNCTION public.create_repository( + project_name text +) +RETURNS repositories +LANGUAGE plpgsql +STRICT +VOLATILE +SECURITY DEFINER +AS $func$ +DECLARE + v_repo repositories%ROWTYPE; +BEGIN + -- 1) Create the repository + INSERT INTO repositories (name) + VALUES (project_name) + RETURNING * INTO v_repo; + + -- 2) Link it to the profile + INSERT INTO profile_repositories (profile, repository, status) + VALUES (auth.uid(), v_repo.id, 'ACCEPTED'); + + -- 3) Return the inserted repo row + RETURN v_repo; +END +$func$; \ No newline at end of file diff --git a/apps/backend/supabase/migrations/20251111163017_create_projects_and_secrets.sql b/apps/backend/supabase/migrations/20251111163017_create_projects_and_secrets.sql new file mode 100644 index 0000000..4130a5c --- /dev/null +++ b/apps/backend/supabase/migrations/20251111163017_create_projects_and_secrets.sql @@ -0,0 +1,41 @@ +create type "public"."project_types" as enum ('SUPABASE', 'VERCEL'); + +create table "public"."projects" ( + "id" uuid not null default gen_random_uuid (), + "name" text not null, + "repository" uuid not null, + "url" text null, + "type" public.project_types not null, + "created_at" timestamp with time zone not null default now(), + constraint projects_pkey primary key (id), + constraint projects_repository_fkey foreign KEY (repository) references repositories (id) +) TABLESPACE pg_default; + +alter table "public"."projects" enable row level security; + +create policy "Enable insert for repo user" +on "public"."projects" +as permissive +for insert +to authenticated +with check ((EXISTS ( SELECT 1 + FROM repositories + WHERE (repositories.id = projects.repository)))); + +create policy "Enabled read for repo member" +on "public"."projects" +as permissive +for select +to authenticated +using ((EXISTS ( SELECT 1 + FROM repositories + WHERE (repositories.id = projects.repository)))); + +create policy "Enable read for users" +on "public"."repositories" +as permissive +for select +to authenticated +using ((EXISTS ( SELECT 1 + FROM profile_repositories + WHERE (profile_repositories.repository = repositories.id)))); \ No newline at end of file diff --git a/apps/frontend/app/page.tsx b/apps/frontend/app/(app)/page.tsx similarity index 58% rename from apps/frontend/app/page.tsx rename to apps/frontend/app/(app)/page.tsx index 295f8fd..eec8c7b 100644 --- a/apps/frontend/app/page.tsx +++ b/apps/frontend/app/(app)/page.tsx @@ -1,6 +1,18 @@ +import { createClient } from "@/utils/supabase/server"; import Image from "next/image"; +import { redirect } from "next/navigation"; + +export default async function Home() { + + const supabase = await createClient(); + const {data: user} = await supabase.auth.getUser(); + console.log('User', user) + const {data, error} = await supabase.from('profile_repositories').select('*').eq('profile', user.user?.id); + + if (data?.length === 0){ + redirect('/wizard'); + } -export default function Home() { return (
@@ -35,29 +47,8 @@ export default function Home() {

- - Vercel logomark - Deploy Now - - - Documentation - + +
diff --git a/apps/frontend/app/(app)/repository/[id]/actions.ts b/apps/frontend/app/(app)/repository/[id]/actions.ts new file mode 100644 index 0000000..5e2628e --- /dev/null +++ b/apps/frontend/app/(app)/repository/[id]/actions.ts @@ -0,0 +1,22 @@ +'use server' + +import { createClient } from "@/utils/supabase/server"; + +export async function createProjectAction(formData: FormData){ + const repositoryId = formData.get('repository') as string; + const name = formData.get('name') as string; + const projectType = formData.get('type') as string; + + console.log(repositoryId, name, projectType); + + const supabase = await createClient(); + const {data, error} = await supabase.from('projects').insert({ + name, + repository: repositoryId, + type: projectType, + }); + if (error){ + console.error(error.message); + throw error; + } +} \ No newline at end of file diff --git a/apps/frontend/app/(app)/repository/[id]/components/projects-card.tsx b/apps/frontend/app/(app)/repository/[id]/components/projects-card.tsx new file mode 100644 index 0000000..e868bdd --- /dev/null +++ b/apps/frontend/app/(app)/repository/[id]/components/projects-card.tsx @@ -0,0 +1,42 @@ +import { AddProjectDialog } from "@/components/add-project-dialog"; +import SupabaseIcon from "@/components/supabase-icon"; +import VercelIcon from "@/components/vercel-icon"; +import { createClient } from "@/utils/supabase/server"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@repo/ui/src/components/card"; +import { Folder } from "lucide-react"; + + +export async function ProjectsCard({ repositoryId }: { repositoryId: string }) { + const supabase = await createClient(); + const { data: projects, error } = await supabase.from('projects').select('*').eq('repository', repositoryId); + if (error){ + console.error(error.message); + throw error; + } + + return ( + + +
+ + Projects + +
+ + {projects.length} project{projects.length !== 1 ? 's' : ''} in this repository + +
+ +
+ {projects.map((project) => ( +
+ {project.type === 'SUPABASE' ? : } + {project.name} +
+ ))} +
+
+
+ ); +} + diff --git a/apps/frontend/app/(app)/repository/[id]/components/users-card.tsx b/apps/frontend/app/(app)/repository/[id]/components/users-card.tsx new file mode 100644 index 0000000..aaea92e --- /dev/null +++ b/apps/frontend/app/(app)/repository/[id]/components/users-card.tsx @@ -0,0 +1,49 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@repo/ui/src/components/card"; +import { Users } from "lucide-react"; + +// Mock data - replace with actual data when available +const mockUsers = [ + { id: '1', name: 'John Doe', email: 'john@example.com' }, + { id: '2', name: 'Jane Smith', email: 'jane@example.com' }, + { id: '3', name: 'Bob Johnson', email: 'bob@example.com' }, +]; + +export async function UsersCard({ repositoryId }: { repositoryId: string }) { + // TODO: Fetch users from database + // const supabase = await createClient(); + // const { data: users } = await supabase.from('repository_users').select('*, users(*)').eq('repository_id', repositoryId); + + const users = mockUsers; + + return ( + + +
+ + Users +
+ + {users.length} user{users.length !== 1 ? 's' : ''} with access + +
+ +
+ {users.map((user) => ( +
+
+ + {user.name.split(' ').map(n => n[0]).join('')} + +
+
+

{user.name}

+

{user.email}

+
+
+ ))} +
+
+
+ ); +} + diff --git a/apps/frontend/app/(app)/repository/[id]/components/variables-card.tsx b/apps/frontend/app/(app)/repository/[id]/components/variables-card.tsx new file mode 100644 index 0000000..ced343e --- /dev/null +++ b/apps/frontend/app/(app)/repository/[id]/components/variables-card.tsx @@ -0,0 +1,50 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@repo/ui/src/components/card"; +import { Key } from "lucide-react"; + +// Mock data - replace with actual data when available +const mockVariables = [ + { id: '1', key: 'DATABASE_URL', value: 'postgresql://...' }, + { id: '2', key: 'API_KEY', value: 'sk-...' }, + { id: '3', key: 'ENVIRONMENT', value: 'production' }, + { id: '4', key: 'DATABASE_URL', value: 'postgresql://...' }, + { id: '5', key: 'API_KEY', value: 'sk-...' }, + { id: '6', key: 'ENVIRONMENT', value: 'production' }, +]; + +export async function VariablesCard({ repositoryId }: { repositoryId: string }) { + // TODO: Fetch variables from database + // const supabase = await createClient(); + // const { data: variables } = await supabase.from('variables').select('*').eq('repository_id', repositoryId); + + const variables = mockVariables; + + return ( + + +
+ + Variables +
+ + {variables.length} environment variable{variables.length !== 1 ? 's' : ''} + +
+ +
+ {variables.map((variable) => ( +
+
+ + {variable.key} +
+

+ {variable.value} +

+
+ ))} +
+
+
+ ); +} + diff --git a/apps/frontend/app/(app)/repository/[id]/page.tsx b/apps/frontend/app/(app)/repository/[id]/page.tsx new file mode 100644 index 0000000..6910b8f --- /dev/null +++ b/apps/frontend/app/(app)/repository/[id]/page.tsx @@ -0,0 +1,45 @@ +'use server' +import { createClient } from "@/utils/supabase/server"; +import { notFound } from "next/navigation"; +import { Suspense } from "react"; +import { ProjectsCard } from "./components/projects-card"; +import { VariablesCard } from "./components/variables-card"; +import { UsersCard } from "./components/users-card"; + +export default async function RepositoryPage({params}: {params: Promise<{id: string}>}){ + const supabase = await createClient(); + const {id} = await params; + const {data, error} = await supabase.from('repositories').select('*').eq('id', id).single(); + if (error){ + console.error(error.message); + throw new Error(error.message); + } + if (!data){ + notFound(); + } + + const repository = data; + + return ( +
+
+

{repository.name}

+

Repository details and configuration

+
+ +
+ }> + + + + }> + + + + }> + + +
+
+ ); +} diff --git a/apps/frontend/app/(app)/wizard/actions.ts b/apps/frontend/app/(app)/wizard/actions.ts new file mode 100644 index 0000000..daeaf5f --- /dev/null +++ b/apps/frontend/app/(app)/wizard/actions.ts @@ -0,0 +1,22 @@ +'use server' + +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + +export async function createRepoAction(formData: FormData){ + const name = formData.get('name') as string; + console.log(name); + + const supabase = await createClient(); + + const {data, error} = await supabase.rpc('create_repository', { + project_name: name, + }); + + if (error){ + console.error(error); + throw new Error(error.message); + } + + redirect(`/repository/${data.id}`); +} \ No newline at end of file diff --git a/apps/frontend/app/(app)/wizard/page.tsx b/apps/frontend/app/(app)/wizard/page.tsx new file mode 100644 index 0000000..dc20066 --- /dev/null +++ b/apps/frontend/app/(app)/wizard/page.tsx @@ -0,0 +1,30 @@ +import { Input } from "@repo/ui/src/components/input"; +import { Label } from "@repo/ui/src/components/label"; +import { Card } from "@repo/ui/src/components/card"; +import { SubmitButton } from "@/components/submit-button"; +import { createRepoAction } from "./actions"; + +export default function WizardPage(){ + return
+ +
+

Create a new repository

+ + + + + + +
+
+} \ No newline at end of file diff --git a/apps/frontend/app/(auth)/login/actions.ts b/apps/frontend/app/(auth)/login/actions.ts new file mode 100644 index 0000000..1ded797 --- /dev/null +++ b/apps/frontend/app/(auth)/login/actions.ts @@ -0,0 +1,29 @@ +'use server' + +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + + +export async function loginAction(formData: FormData) { + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + console.log(email, password); + + if (!email || !password){ + throw new Error("Missing required fields"); + } + + const supabase = await createClient(); + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error){ + console.error(error); + throw new Error(error.message); + } + + redirect("/"); +} \ No newline at end of file diff --git a/apps/frontend/app/(auth)/login/components/login-form.tsx b/apps/frontend/app/(auth)/login/components/login-form.tsx new file mode 100644 index 0000000..6c1b5f5 --- /dev/null +++ b/apps/frontend/app/(auth)/login/components/login-form.tsx @@ -0,0 +1,65 @@ +import { cn } from "@workspace/ui/lib/utils" +import { Button } from "@workspace/ui/components/button" +import { + Field, + FieldDescription, + FieldGroup, + FieldLabel, + FieldSeparator, + } from "@workspace/ui/components/field" +import { Input } from "@workspace/ui/components/input" +import Link from "next/link" +import { loginAction } from "../actions" +export function LoginForm({ + className, + ...props +}: React.ComponentProps<"form">) { + return ( +
+ +
+

Login to your account

+

+ Enter your email below to login to your account +

+
+ + Email + + + +
+ Password +
+ + + Forgot your password? + +
+ + + + Or continue with + + + + Don't have an account?{" "} + Sign up + + + +
+
+ ) +} \ No newline at end of file diff --git a/apps/frontend/app/(auth)/login/page.tsx b/apps/frontend/app/(auth)/login/page.tsx new file mode 100644 index 0000000..bcb1218 --- /dev/null +++ b/apps/frontend/app/(auth)/login/page.tsx @@ -0,0 +1,32 @@ +import { GalleryVerticalEnd } from "lucide-react" +import { LoginForm } from "./components/login-form" + + +export default function SignupPage() { + return ( +
+
+
+ +
+ +
+ Acme Inc. +
+
+
+
+ +
+
+
+
+ Image +
+
+ ) +} \ No newline at end of file diff --git a/apps/frontend/app/(auth)/signup/actions/actions.ts b/apps/frontend/app/(auth)/signup/actions/actions.ts new file mode 100644 index 0000000..3b3c7ca --- /dev/null +++ b/apps/frontend/app/(auth)/signup/actions/actions.ts @@ -0,0 +1,59 @@ +'use server' +import { createClient } from "@/utils/supabase/server"; +import { redirect } from "next/navigation"; + +export async function loginAction(formData: FormData) { + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + console.log(email, password); + + if (!email || !password){ + throw new Error("Missing required fields"); + } + + const supabase = await createClient(); + const { data, error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + + if (error){ + console.error(error); + throw new Error(error.message); + } + + redirect("/"); +} + +export async function signupAction(formData: FormData) { + const name = formData.get("name") as string; + const email = formData.get("email") as string; + const password = formData.get("password") as string; + + + console.log(name, email, password); + + if (!name || !email || !password){ + + throw new Error("Missing required fields"); + } + + const supabase = await createClient(); + const { data, error } = await supabase.auth.signUp({ + email, + password, + options: { + data: { + full_name: name, + }, + }, + }); + + if (error){ + console.error(error); + throw new Error(error.message); + } + + redirect("/"); +} \ No newline at end of file diff --git a/apps/frontend/app/(auth)/signup/components/signup-form.tsx b/apps/frontend/app/(auth)/signup/components/signup-form.tsx new file mode 100644 index 0000000..1632c77 --- /dev/null +++ b/apps/frontend/app/(auth)/signup/components/signup-form.tsx @@ -0,0 +1,74 @@ +import { cn } from "@workspace/ui/lib/utils" + +import { Button } from "@workspace/ui/components/button" +import { + Field, + FieldDescription, + FieldGroup, + FieldLabel, + FieldSeparator, +} from "@workspace/ui/components/field" + +import { Input } from "@workspace/ui/components/input" +import Link from "next/link" +import { signupAction } from "../actions/actions" + +export function SignupForm({ + className, + ...props +}: React.ComponentProps<"form">) { + return ( +
+ +
+

Create your account

+

+ Fill in the form below to create your account +

+
+ + Full Name + + + + Email + + + We'll use this to contact you. We will not share your email + with anyone else. + + + + Password + + + Must be at least 8 characters long. + + + + Confirm Password + + Please confirm your password. + + + + + Or continue with + + + + Already have an account? Sign in + + +
+
+ ) +} diff --git a/apps/frontend/app/(auth)/signup/page.tsx b/apps/frontend/app/(auth)/signup/page.tsx new file mode 100644 index 0000000..67e94be --- /dev/null +++ b/apps/frontend/app/(auth)/signup/page.tsx @@ -0,0 +1,31 @@ +import { GalleryVerticalEnd } from "lucide-react" +import { SignupForm } from "../signup/components/signup-form" + +export default function SignupPage() { + return ( +
+
+
+ +
+ +
+ Senv.dev +
+
+
+
+ +
+
+
+
+ Image +
+
+ ) +} \ No newline at end of file diff --git a/apps/frontend/app/login/page.tsx b/apps/frontend/app/login/page.tsx deleted file mode 100644 index 19f8dd7..0000000 --- a/apps/frontend/app/login/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Button } from "@workspace/ui/components/button" - -import Link from "next/link"; - -export default function LoginPage() { - return ( -
-

Login

- Link to open - -
- ) -} \ No newline at end of file diff --git a/apps/frontend/components/add-project-dialog.tsx b/apps/frontend/components/add-project-dialog.tsx new file mode 100644 index 0000000..328f925 --- /dev/null +++ b/apps/frontend/components/add-project-dialog.tsx @@ -0,0 +1,114 @@ +'use client' + +import { Button } from "@repo/ui/src/components/button"; +import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@repo/ui/src/components/dialog"; +import { Input } from "@repo/ui/src/components/input"; +import { Label } from "@repo/ui/src/components/label"; +import { PlusIcon } from "lucide-react"; +import { SubmitButton } from "./submit-button"; +import { createProjectAction } from "@/app/(app)/repository/[id]/actions"; +import { ProjectTypePicker } from "./project-type-picker"; + +import { useForm } from "@tanstack/react-form" + +import * as z from "zod" +import { Field, FieldError, FieldGroup, FieldLabel } from "@repo/ui/src/components/field"; +import { Spinner } from "@repo/ui/src/components/spinner"; + + +const formSchema = z.object({ + name: z + .string() + .min(3, "Name of the project must be at least 5 characters.") + .max(32, "The name of the project must be at most 32 characters."), + type: z.enum(["SUPABASE", "VERCEL"]), + }) + +export function AddProjectDialog({repositoryId,}: {repositoryId: string,}) { + + const form = useForm({ + defaultValues: { + name: "", + type: "", + }, + validators: { + onSubmit: formSchema, + onBlur: formSchema, + }, + onSubmit: async ({ value }) => { + const formData = new FormData(); + formData.append('name', value.name); + formData.append('type', value.type); + formData.append('repository', repositoryId); + await createProjectAction(formData) + } + }) + + return + + + + +
+ + Create project + + Create a new project for this repository. + + +
+
+ +
+
+ + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid + return + Name + field.handleChange(e.target.value)} + required + /> + {isInvalid && ( + + )} + + }}/> + + + + { + const isInvalid = + field.state.meta.isTouched && !field.state.meta.isValid + return + Type + field.handleChange(value)} + /> + + }}/> + +
+
+ + + + + + {{ + return + }}}/> + + + +
+
+
+} \ No newline at end of file diff --git a/apps/frontend/components/project-type-picker.tsx b/apps/frontend/components/project-type-picker.tsx new file mode 100644 index 0000000..f860fa1 --- /dev/null +++ b/apps/frontend/components/project-type-picker.tsx @@ -0,0 +1,82 @@ +"use client" + +import * as React from "react" +import { Check, ChevronsUpDown } from "lucide-react" + +import { cn } from "@repo/ui/src/lib/utils" +import { Button } from "@repo/ui/src/components/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@repo/ui/src/components/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@repo/ui/src/components/popover" + +const frameworks = [ + { + value: "SUPABASE", + label: "Supabase", + }, + { + value: "VERCEL", + label: "Vercel", + } +] + +export function ProjectTypePicker({defaultValue, onChange}: {defaultValue: "VERCEL" | "SUPABASE", onChange: (value: "VERCEL" | "SUPABASE") => void}) { + const [open, setOpen] = React.useState(false) + const [value, setValue] = React.useState<"VERCEL" | "SUPABASE">(defaultValue) + + return ( + + + + + + + + + No framework found. + + {frameworks.map((framework) => ( + { + setValue(currentValue === value ? "SUPABASE" : currentValue as "VERCEL" | "SUPABASE") + setOpen(false) + }} + > + {framework.label} + + + ))} + + + + + + ) +} diff --git a/apps/frontend/components/submit-button.tsx b/apps/frontend/components/submit-button.tsx new file mode 100644 index 0000000..b847a15 --- /dev/null +++ b/apps/frontend/components/submit-button.tsx @@ -0,0 +1,21 @@ +'use client' + +import { useFormStatus } from 'react-dom' + +import { Button } from '@repo/ui/src/components/button' +import { Spinner } from '@repo/ui/src/components/spinner' + +export function SubmitButton({ + text, +}: { + text: string +}) { + const { pending } = useFormStatus() + + return ( + + ) +} diff --git a/apps/frontend/components/supabase-icon.tsx b/apps/frontend/components/supabase-icon.tsx new file mode 100644 index 0000000..34f9dfe --- /dev/null +++ b/apps/frontend/components/supabase-icon.tsx @@ -0,0 +1,8 @@ + +export default function SupabaseIcon(){ + return + {/* Boxicons v3.0.3 https://boxicons.com | License https://docs.boxicons.com/free */} + + +} \ No newline at end of file diff --git a/apps/frontend/components/vercel-icon.tsx b/apps/frontend/components/vercel-icon.tsx new file mode 100644 index 0000000..703fe50 --- /dev/null +++ b/apps/frontend/components/vercel-icon.tsx @@ -0,0 +1,9 @@ + + +export default function VercelIcon(){ + return + {/* */} + + +} \ No newline at end of file diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 11cae14..833cf36 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -9,9 +9,10 @@ "lint": "eslint" }, "dependencies": { - "@workspace/ui": "workspace:*", "@supabase/ssr": "^0.7.0", "@supabase/supabase-js": "^2.81.0", + "@tanstack/react-form": "^1.23.8", + "@workspace/ui": "workspace:*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.553.0", @@ -19,15 +20,16 @@ "react": "19.2.0", "react-dom": "19.2.0", "tailwind-merge": "^3.4.0", - "tw-animate-css": "^1.4.0" + "tw-animate-css": "^1.4.0", + "zod": "^3.25.76" }, "devDependencies": { - "@workspace/eslint-config": "workspace:*", - "@workspace/typescript-config": "workspace:*", "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@workspace/eslint-config": "workspace:*", + "@workspace/typescript-config": "workspace:*", "eslint": "^9", "eslint-config-next": "16.0.1", "tailwindcss": "^4", diff --git a/apps/frontend/middleware.ts b/apps/frontend/proxy.ts similarity index 74% rename from apps/frontend/middleware.ts rename to apps/frontend/proxy.ts index c2a004d..9928eeb 100644 --- a/apps/frontend/middleware.ts +++ b/apps/frontend/proxy.ts @@ -1,6 +1,7 @@ import { type NextRequest } from 'next/server' import { updateSession } from '@/utils/supabase/middleware' -export async function middleware(request: NextRequest) { + +export async function proxy(request: NextRequest) { // update user's auth session return await updateSession(request) } @@ -13,6 +14,6 @@ export const config = { * - favicon.ico (favicon file) * Feel free to modify this pattern to include more paths. */ - '/((?!_next/static|_next/image|favicon.ico|login(?:/.*)?|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', + '/((?!_next/static|_next/image|favicon.ico|login(?:/.*)?|signup(?:/.*)?|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)', ], } \ No newline at end of file diff --git a/apps/frontend/utils/supabase/middleware.ts b/apps/frontend/utils/supabase/middleware.ts index 42c9c91..1671958 100644 --- a/apps/frontend/utils/supabase/middleware.ts +++ b/apps/frontend/utils/supabase/middleware.ts @@ -25,6 +25,9 @@ export async function updateSession(request: NextRequest) { } ) // refreshing the auth token - await supabase.auth.getUser() + const { error } = await supabase.auth.getUser() + if (error){ + return NextResponse.redirect(new URL('/login', request.url)) + } return supabaseResponse } \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index 944ae93..fca3f40 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -7,12 +7,17 @@ "lint": "eslint . --max-warnings 0" }, "dependencies": { - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "cmdk": "^1.1.1", "lucide-react": "^0.475.0", "next-themes": "^0.4.6", - "react": "^19.1.1", + "react": "^19.2.0", "react-dom": "^19.1.1", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.3.6", @@ -22,7 +27,7 @@ "@tailwindcss/postcss": "^4.1.11", "@turbo/gen": "^2.5.5", "@types/node": "^20.19.9", - "@types/react": "^19.1.9", + "@types/react": "^19.2.2", "@types/react-dom": "^19.1.7", "@workspace/eslint-config": "workspace:*", "@workspace/typescript-config": "workspace:*", diff --git a/packages/ui/src/components/card.tsx b/packages/ui/src/components/card.tsx new file mode 100644 index 0000000..2e7392a --- /dev/null +++ b/packages/ui/src/components/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@workspace/ui/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/packages/ui/src/components/command.tsx b/packages/ui/src/components/command.tsx new file mode 100644 index 0000000..369c620 --- /dev/null +++ b/packages/ui/src/components/command.tsx @@ -0,0 +1,184 @@ +"use client" + +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "@workspace/ui/lib/utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@workspace/ui/components/dialog" + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps & { + title?: string + description?: string + className?: string + showCloseButton?: boolean +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/packages/ui/src/components/dialog.tsx b/packages/ui/src/components/dialog.tsx new file mode 100644 index 0000000..b905322 --- /dev/null +++ b/packages/ui/src/components/dialog.tsx @@ -0,0 +1,143 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@workspace/ui/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/packages/ui/src/components/field.tsx b/packages/ui/src/components/field.tsx new file mode 100644 index 0000000..36cb298 --- /dev/null +++ b/packages/ui/src/components/field.tsx @@ -0,0 +1,248 @@ +"use client" + +import { useMemo } from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@workspace/ui/lib/utils" +import { Label } from "@workspace/ui/components/label" +import { Separator } from "@workspace/ui/components/separator" + +function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { + return ( +
[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3", + className + )} + {...props} + /> + ) +} + +function FieldLegend({ + className, + variant = "legend", + ...props +}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { + return ( + + ) +} + +function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
[data-slot=field-group]]:gap-4", + className + )} + {...props} + /> + ) +} + +const fieldVariants = cva( + "group/field flex w-full gap-3 data-[invalid=true]:text-destructive", + { + variants: { + orientation: { + vertical: ["flex-col [&>*]:w-full [&>.sr-only]:w-auto"], + horizontal: [ + "flex-row items-center", + "[&>[data-slot=field-label]]:flex-auto", + "has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + responsive: [ + "flex-col [&>*]:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:[&>*]:w-auto", + "@md/field-group:[&>[data-slot=field-label]]:flex-auto", + "@md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + ], + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +) + +function Field({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} + +function FieldContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function FieldLabel({ + className, + ...props +}: React.ComponentProps) { + return ( +