create database connection

This commit is contained in:
2024-09-18 18:38:35 -04:00
parent dab127aed7
commit 0e019e3c30
15 changed files with 557 additions and 607 deletions

11
.env
View File

@@ -2,12 +2,15 @@
# should be updated accordingly.
# Drizzle
DATABASE_URL="postgresql://postgres:password@localhost:5432/hristudio"
DATABASE_URL="postgresql://postgres:jusxah-jufrew-niwjY5@db:5432/hristudio"
# Clerk
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_cmVmaW5lZC1kcnVtLTIzLmNsZXJrLmFjY291bnRzLmRldiQ
CLERK_SECRET_KEY=sk_test_3qESERGxZqHpROHzFe7nYxjfqfVhpHWS1UVDQt86v8
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
# Example:
# SERVERVAR="foo"
# NEXT_PUBLIC_CLIENTVAR="bar"
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=jusxah-jufrew-niwjY5
POSTGRES_DB=hristudio

5
.gitignore vendored
View File

@@ -53,4 +53,7 @@ psd
thumb
sketch
# End of https://www.toptal.com/developers/gitignore/api/nextjs,react
pnpm-lock.yaml
# End of https://www.toptal.com/developers/gitignore/api/nextjs,react
pnpm-lock.yaml

View File

@@ -14,7 +14,7 @@
"start": "next start"
},
"dependencies": {
"@clerk/nextjs": "^5.5.2",
"@clerk/nextjs": "^5.6.0",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-icons": "^1.3.0",
@@ -25,9 +25,9 @@
"clsx": "^2.1.1",
"cn": "^0.1.1",
"drizzle-orm": "^0.33.0",
"geist": "^1.3.0",
"geist": "^1.3.1",
"lucide-react": "^0.441.0",
"next": "^14.2.4",
"next": "^14.2.12",
"next-themes": "^0.3.0",
"postgres": "^3.4.4",
"react": "^18.3.1",
@@ -35,24 +35,24 @@
"react-icons": "^5.3.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.3"
"zod": "^3.23.8"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/eslint": "^8.56.12",
"@types/node": "^20.16.5",
"@types/react": "^18.3.7",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"drizzle-kit": "^0.24.0",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.4",
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.6.0",
"drizzle-kit": "^0.24.2",
"eslint": "^8.57.1",
"eslint-config-next": "^14.2.12",
"eslint-plugin-drizzle": "^0.2.3",
"postcss": "^8.4.39",
"prettier": "^3.3.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"tailwindcss": "^3.4.3",
"typescript": "^5.5.3"
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.6",
"tailwindcss": "^3.4.12",
"typescript": "^5.6.2"
},
"ct3aMetadata": {
"initVersion": "7.37.0"

858
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
import { db } from "~/server/db";
import { studies } from "~/server/db/schema";
import { NextResponse } from "next/server";
import { eq } from "drizzle-orm";
export async function GET() {
const allStudies = await db.select().from(studies);
return NextResponse.json(allStudies);
}
export async function POST(request: Request) {
const { title, description } = await request.json();
const newStudy = await db.insert(studies).values({ title, description }).returning();
return NextResponse.json(newStudy[0]);
}
export async function PUT(request: Request) {
const { id, title, description } = await request.json();
const updatedStudy = await db
.update(studies)
.set({ title, description })
.where(eq(studies.id, id))
.returning();
return NextResponse.json(updatedStudy[0]);
}
export async function DELETE(request: Request) {
const { id } = await request.json();
await db.delete(studies).where(eq(studies.id, id));
return NextResponse.json({ message: "Study deleted" });
}

View File

@@ -1,9 +1,15 @@
import { type PropsWithChildren } from "react"
import { Sidebar } from "~/components/sidebar"
import { inter } from "../layout"
import { Inter } from "next/font/google"
import "~/styles/globals.css"
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-sans",
})
export default function RootLayout({ children }: PropsWithChildren) {
return (
<html lang="en">

View File

@@ -1,5 +1,6 @@
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '~/components/ui/card';
import { Button } from '~/components/ui/button';
import { Studies } from "~/components/Studies";
const HomePage: React.FC = () => {
return (
@@ -11,41 +12,7 @@ const HomePage: React.FC = () => {
Manage your Human-Robot Interaction projects and experiments
</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card className="bg-white shadow-lg">
<CardHeader>
<CardTitle className="text-2xl font-semibold text-blue-700">Projects</CardTitle>
<CardDescription>Manage your HRI projects</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4">Create, edit, and analyze your HRI projects.</p>
<Button className="bg-blue-600 hover:bg-blue-700 text-white">View Projects</Button>
</CardContent>
</Card>
<Card className="bg-white shadow-lg">
<CardHeader>
<CardTitle className="text-2xl font-semibold text-blue-700">Experiments</CardTitle>
<CardDescription>Design and run experiments</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4">Set up, conduct, and analyze HRI experiments.</p>
<Button className="bg-green-600 hover:bg-green-700 text-white">New Experiment</Button>
</CardContent>
</Card>
<Card className="bg-white shadow-lg">
<CardHeader>
<CardTitle className="text-2xl font-semibold text-blue-700">Data Analysis</CardTitle>
<CardDescription>Analyze your research data</CardDescription>
</CardHeader>
<CardContent>
<p className="mb-4">Visualize and interpret your HRI research data.</p>
<Button className="bg-purple-600 hover:bg-purple-700 text-white">Analyze Data</Button>
</CardContent>
</Card>
</div>
<Studies />
</div>
</div>
);

View File

@@ -1,10 +1,9 @@
import { ClerkProvider } from '@clerk/nextjs'
import { Inter } from "next/font/google"
import { ThemeProvider } from "next-themes"
import "~/styles/globals.css"
export const inter = Inter({
const inter = Inter({
subsets: ["latin"],
display: "swap",
variable: "--font-sans",

View File

@@ -52,7 +52,7 @@ export default function HomePage() {
<div className="text-center mb-12">
<h2 className="text-3xl font-semibold mb-4 text-blue-700">Join the HRI Revolution</h2>
<p className="text-lg text-gray-700 mb-6">
Whether you're a seasoned researcher or just starting in the field of Human-Robot Interaction,
Whether you&apos;re a seasoned researcher or just starting in the field of Human-Robot Interaction,
HRIStudio provides the tools and support you need to succeed.
</p>
<div className="space-x-4">

View File

@@ -31,8 +31,9 @@ export default function SignInPage() {
await setActive({ session: result.createdSessionId })
router.push("/dash")
}
} catch (err: any) {
console.error("Error:", err.errors[0].message)
} catch (err) {
const error = err as { errors?: { message: string }[] };
console.error("Error:", error.errors?.[0]?.message ?? "Unknown error")
}
}
@@ -42,6 +43,8 @@ export default function SignInPage() {
strategy,
redirectUrl: "/sso-callback",
redirectUrlComplete: "/dash",
}).catch((error) => {
console.error("Authentication error:", error); // Handle any potential errors
})
}
@@ -90,7 +93,7 @@ export default function SignInPage() {
</CardContent>
<CardFooter className="flex flex-col">
<p className="mt-4 text-sm text-center">
Don't have an account?{" "}
Don&apos;t have an account?{" "}
<Link href="/sign-up" className="text-blue-600 hover:underline">
Sign up
</Link>

View File

@@ -31,8 +31,9 @@ export default function SignUpPage() {
await setActive({ session: result.createdSessionId })
router.push("/dash")
}
} catch (err: any) {
console.error("Error:", err.errors[0].message)
} catch (err) {
const error = err as { errors?: { message: string }[] }; // Specify type
console.error("Error:", error.errors?.[0]?.message ?? "Unknown error") // Use optional chaining
}
}
@@ -42,6 +43,8 @@ export default function SignUpPage() {
strategy,
redirectUrl: "/sso-callback",
redirectUrlComplete: "/dash",
}).catch((error) => {
console.error("Authentication error:", error); // Handle any potential errors
})
}

122
src/components/Studies.tsx Normal file
View File

@@ -0,0 +1,122 @@
"use client";
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "~/components/ui/dialog";
interface Study {
id: number;
title: string;
description: string;
}
export function Studies() {
const [studies, setStudies] = useState<Study[]>([]);
const [newStudy, setNewStudy] = useState({ title: '', description: '' });
const [editingStudy, setEditingStudy] = useState<Study | null>(null);
useEffect(() => {
fetchStudies();
}, []);
const fetchStudies = async () => {
const response = await fetch('/api/studies');
const data = await response.json();
setStudies(data);
};
const createStudy = async () => {
const response = await fetch('/api/studies', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newStudy),
});
const createdStudy = await response.json();
setStudies([...studies, createdStudy]);
setNewStudy({ title: '', description: '' });
};
const updateStudy = async () => {
if (!editingStudy) return;
const response = await fetch('/api/studies', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(editingStudy),
});
const updatedStudy = await response.json();
setStudies(studies.map(s => s.id === updatedStudy.id ? updatedStudy : s));
setEditingStudy(null);
};
const deleteStudy = async (id: number) => {
await fetch('/api/studies', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
});
setStudies(studies.filter(s => s.id !== id));
};
return (
<div className="space-y-4">
<h2 className="text-2xl font-bold">Studies</h2>
<Dialog>
<DialogTrigger asChild>
<Button>Create New Study</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Study</DialogTitle>
</DialogHeader>
<Input
placeholder="Title"
value={newStudy.title}
onChange={(e) => setNewStudy({ ...newStudy, title: e.target.value })}
/>
<Input
placeholder="Description"
value={newStudy.description}
onChange={(e) => setNewStudy({ ...newStudy, description: e.target.value })}
/>
<Button onClick={createStudy}>Create</Button>
</DialogContent>
</Dialog>
{studies.map((study) => (
<Card key={study.id}>
<CardHeader>
<CardTitle>{study.title}</CardTitle>
</CardHeader>
<CardContent>
<p>{study.description}</p>
<div className="flex space-x-2 mt-2">
<Dialog>
<DialogTrigger asChild>
<Button variant="outline" onClick={() => setEditingStudy(study)}>Edit</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Study</DialogTitle>
</DialogHeader>
<Input
placeholder="Title"
value={editingStudy?.title || ''}
onChange={(e) => setEditingStudy({ ...editingStudy!, title: e.target.value })}
/>
<Input
placeholder="Description"
value={editingStudy?.description || ''}
onChange={(e) => setEditingStudy({ ...editingStudy!, description: e.target.value })}
/>
<Button onClick={updateStudy}>Update</Button>
</DialogContent>
</Dialog>
<Button variant="destructive" onClick={() => deleteStudy(study.id)}>Delete</Button>
</div>
</CardContent>
</Card>
))}
</div>
);
}

View File

@@ -61,8 +61,8 @@ export function Sidebar() {
<div className="flex items-center space-x-4">
<UserButton />
<div>
<p className="text-sm font-medium text-blue-800">{user?.fullName || 'User'}</p>
<p className="text-xs text-blue-600">{user?.primaryEmailAddress?.emailAddress || 'user@example.com'}</p>
<p className="text-sm font-medium text-blue-800">{user?.fullName ?? 'User'}</p>
<p className="text-xs text-blue-600">{user?.primaryEmailAddress?.emailAddress ?? 'user@example.com'}</p>
</div>
</div>
</div>

View File

@@ -2,10 +2,8 @@ import * as React from "react"
import { cn } from "~/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
// Use React.InputHTMLAttributes<HTMLInputElement> directly in the component
const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLInputElement>>(
({ className, type, ...props }, ref) => {
return (
<input

View File

@@ -1,13 +1,16 @@
// Example model schema from the Drizzle docs
// https://orm.drizzle.team/docs/sql-schema-declaration
import { sql } from "drizzle-orm";
import { pgTableCreator } from "drizzle-orm/pg-core";
import { ColumnBaseConfig, ColumnDataType, SQL, sql } from "drizzle-orm";
import {
index,
pgTableCreator,
pgTable,
serial,
integer,
timestamp,
varchar,
ExtraConfigColumn,
} from "drizzle-orm/pg-core";
/**
@@ -30,7 +33,25 @@ export const posts = createTable(
() => new Date()
),
},
(example) => ({
(example: { name: SQL<unknown> | Partial<ExtraConfigColumn<ColumnBaseConfig<ColumnDataType, string>>>; }) => ({
nameIndex: index("name_idx").on(example.name),
})
);
export const studies = createTable(
"study",
{
id: serial("id").primaryKey(),
title: varchar("title", { length: 256 }).notNull(),
description: varchar("description", { length: 1000 }),
createdAt: timestamp("created_at", { withTimezone: true })
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
() => new Date()
),
},
(study: { title: SQL<unknown> | Partial<ExtraConfigColumn<ColumnBaseConfig<ColumnDataType, string>>>; }) => ({
titleIndex: index("title_idx").on(study.title),
})
);