Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CRUD for projects #10

Merged
merged 1 commit into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
serverActions: {
bodySizeLimit: '2mb',
},
},
redirects() {
return [
{
Expand Down
2,036 changes: 1,956 additions & 80 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@
"@radix-ui/react-slot": "^1.0.2",
"@supabase/auth-helpers-nextjs": "^0.8.1",
"@vercel/analytics": "^1.1.1",
"date-fns": "2.30.0",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"core-js": "^3.33.2",
"date-fns": "2.30.0",
"langchain": "^0.0.163",
"lucide-react": "^0.284.0",
"mobx": "^6.10.2",
Expand All @@ -46,14 +47,15 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0",
"swagger-ui-react": "^5.9.0",
"redoc": "^2.1.3",
"styled-components": "^6.1.0",
"swagger-ui-react": "^5.9.0",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/node": "^20.8.0",
"@types/pg": "^8.10.5",
"@types/react": "^18.2.23",
Expand Down
69 changes: 69 additions & 0 deletions src/app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
'use server';

import { createServerComponentClient } from '@supabase/auth-helpers-nextjs';
import bcrypt from 'bcrypt';
import { Database } from '@/types/supabase';
import crypto from 'crypto';
import { revalidatePath } from 'next/cache';
import { cookies } from 'next/headers';

export async function generateApiKey() {
return crypto.randomBytes(32).toString('hex');
}

export const createProject = async (prevState: any, formData: FormData) => {
const supabase = createServerComponentClient<Database>({ cookies });
const name = formData.get('project-name') as string;
const description = formData.get('project-description') as string;

const apiKey = await generateApiKey();

const salt = await bcrypt.genSalt(10);
const encryptedApiKey = await bcrypt.hash(apiKey, salt);

const {
data: { user },
} = await supabase.auth.getUser();

if (!user) {
return { error: 'User not found' };
}

try {
const results = await supabase
.from('projects')
.insert({ name, description, api_key: encryptedApiKey, user_id: user.id })
.select();

if (results.error) {
throw `Error fetching data: projects: ${results.error?.message}`;
}

return { success: true, api_key: apiKey };
} catch (error: any) {
console.log(error);
return { error };
}
};

export const deleteProject = async (prevState: any, formData: FormData) => {
const projectId = formData.get('project-id') as string;
const supabase = createServerComponentClient<Database>({ cookies });

try {
const results = await supabase
.from('projects')
.delete()
.match({ id: projectId })
.select();

if (results.error) {
throw `Error fetching data: projects: ${results.error?.message}`;
}

revalidatePath('/dashboard');
return { success: true };
} catch (error: any) {
return { error: error.message };
}
};
7 changes: 7 additions & 0 deletions src/app/api/projects/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';

export function POST() {
return NextResponse.json({
message: 'POST',
});
}
5 changes: 2 additions & 3 deletions src/app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

import Image from 'next/image';
import { MainNav } from './components/main-nav';
import { UserNav } from './components/user-nav';
Expand All @@ -9,10 +7,11 @@ import { ProjectNav } from './components/project-nav';

export default function MainLayout({
children,
params,
}: {
children: React.ReactNode;
params: { projectId: string };
}) {
const params = useParams();
return (
<div className="relative flex min-h-screen flex-col">
<div className="flex-1">
Expand Down
64 changes: 64 additions & 0 deletions src/app/dashboard/projects/menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use client';

import Button from '@/components/Button';
import { useState } from 'react';
import { deleteProject } from '@/app/actions';
import {
experimental_useFormStatus as useFormStatus,
// @ts-expect-error
experimental_useFormState as useFormState,
} from 'react-dom';
import { Project } from '@/types/supabase-entities';

const initialState = {
message: null,
};

export default function Menu({
children,
project,
}: {
children?: React.ReactNode;
project: Project;
}) {
// state for modal
const [isOpen, setIsOpen] = useState(false);
const [state, formAction] = useFormState(deleteProject, initialState);

return (
<>
<div className="flex items-center space-x-2">
<Button
onClick={() => {
setIsOpen(true);
}}
>
delete
</Button>
</div>
{isOpen && (
<form action={formAction}>
<input type="hidden" name="project-id" value={project.id} />
<div className="fixed inset-0 z-50 flex items-center justify-center w-full h-full bg-black bg-opacity-50 text-slate-950">
<div className="relative z-50 w-11/12 p-8 bg-white border border-gray-300 rounded-lg">
<h2 className="text-xl font-semibold text-center">
Are you sure you want to delete this project?
</h2>
<div className="flex justify-center mt-4 space-x-4">
<Button
variant="default"
onClick={() => {
setIsOpen(false);
}}
>
Cancel
</Button>
<Button variant="destructive">Delete</Button>
</div>
</div>
</div>
</form>
)}
</>
);
}
67 changes: 67 additions & 0 deletions src/app/dashboard/projects/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client';
import Button from '@/components/Button';
import Input from '@/components/Input';
import { createProject } from '@/app/actions';
import {
// @ts-expect-error
experimental_useFormState as useFormState,
} from 'react-dom';
import { useState } from 'react';
import { CopyIcon, CheckIcon } from 'lucide-react';

const initialState = {
error: null,
};

export default function NewProject() {
const [state, formAction] = useFormState(createProject, initialState);
const [copied, setCopied] = useState(false);

console.log({ state });

return (
<>
<div className="flex flex-col items-center">
<h1 className="text-3xl font-bold">New Project</h1>
<div className="flex flex-col items-center">
<form action={formAction}>
<Input
id="project-name"
placeholder="project name"
autoCapitalize="none"
name="project-name"
required
/>
<Input
id="project-description"
placeholder="Project description"
autoCapitalize="none"
name="project-description"
/>
<Button type="submit">Create Project</Button>
</form>

{state.api_key && (
<div className="text-sm text-gray-500">
Copy the api key now. You'll never see it again.
<br />
{state.api_key}
<Button
onClick={() => {
navigator.clipboard.writeText(state.api_key);
setCopied(true);
}}
>
{copied ? <CheckIcon /> : <CopyIcon />}
</Button>
</div>
)}

{state.error && (
<div className="text-sm text-gray-500">{state.error}</div>
)}
</div>
</div>
</>
);
}
Loading