mirror of
https://github.com/soconnor0919/hristudio.git
synced 2025-12-12 07:04:44 -05:00
Form implementation, api routes
This commit is contained in:
4
.env
4
.env
@@ -5,8 +5,8 @@
|
|||||||
DATABASE_URL="postgresql://postgres:jusxah-jufrew-niwjY5@db:5432/hristudio"
|
DATABASE_URL="postgresql://postgres:jusxah-jufrew-niwjY5@db:5432/hristudio"
|
||||||
|
|
||||||
# Clerk
|
# Clerk
|
||||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_YWxsb3dlZC1zYWxtb24tNjMuY2xlcmsuYWNjb3VudHMuZGV2JA
|
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_cmVmaW5lZC1kcnVtLTIzLmNsZXJrLmFjY291bnRzLmRldiQ
|
||||||
CLERK_SECRET_KEY=sk_test_nUKl0GTM5ibgUH12WbTH6pNVHAyRshlSFi64IrEeWD
|
CLERK_SECRET_KEY=sk_test_3qESERGxZqHpROHzFe7nYxjfqfVhpHWS1UVDQt86v8
|
||||||
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
|
||||||
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
|
||||||
|
|
||||||
|
|||||||
14
Dockerfile
14
Dockerfile
@@ -1,6 +1,9 @@
|
|||||||
# Use the Node.js 18 Alpine Linux image as the base image
|
# Use the Node.js 18 Alpine Linux image as the base image
|
||||||
FROM node:22-alpine
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
# Install GraphicsMagick
|
||||||
|
RUN apk add --no-cache graphicsmagick ghostscript
|
||||||
|
|
||||||
# Set the working directory inside the container to /app
|
# Set the working directory inside the container to /app
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -14,8 +17,15 @@ RUN pnpm install
|
|||||||
# Copy all the files from the local directory to the working directory in the container
|
# Copy all the files from the local directory to the working directory in the container
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Push database schema to database
|
# # Clear previous build artifacts
|
||||||
RUN pnpm drizzle-kit push
|
# RUN rm -rf .next
|
||||||
|
|
||||||
|
# # Build the application
|
||||||
|
# RUN pnpm build
|
||||||
|
|
||||||
|
# # Ensure correct permissions
|
||||||
|
# RUN chown -R node:node .
|
||||||
|
# USER node
|
||||||
|
|
||||||
# Run the application in development mode
|
# Run the application in development mode
|
||||||
CMD ["pnpm", "run", "dev"]
|
CMD ["pnpm", "run", "dev"]
|
||||||
@@ -8,5 +8,5 @@ export default {
|
|||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: env.DATABASE_URL,
|
url: env.DATABASE_URL,
|
||||||
},
|
},
|
||||||
tablesFilter: ["hristudio_*"],
|
// tablesFilter: ["hristudio_*"],
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|||||||
@@ -4,7 +4,38 @@
|
|||||||
*/
|
*/
|
||||||
await import("./src/env.js");
|
await import("./src/env.js");
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const config = {};
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
experimental: {
|
||||||
|
serverActions: {
|
||||||
|
bodySizeLimit: '2mb',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
webpack: (config) => {
|
||||||
|
config.externals.push({
|
||||||
|
"utf-8-validate": "commonjs utf-8-validate",
|
||||||
|
bufferutil: "commonjs bufferutil",
|
||||||
|
});
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
// Add this section to disable linting during build
|
||||||
|
eslint: {
|
||||||
|
ignoreDuringBuilds: true,
|
||||||
|
},
|
||||||
|
// Add this section to disable type checking during build
|
||||||
|
typescript: {
|
||||||
|
ignoreBuildErrors: true,
|
||||||
|
},
|
||||||
|
// Add this section
|
||||||
|
async rewrites() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: '/content/:path*',
|
||||||
|
destination: '/api/content/:path*',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default config;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -34,11 +34,13 @@
|
|||||||
"lucide-react": "^0.441.0",
|
"lucide-react": "^0.441.0",
|
||||||
"next": "^14.2.12",
|
"next": "^14.2.12",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"pdf2pic": "^3.1.3",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"radix-ui": "^1.0.1",
|
"radix-ui": "^1.0.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-icons": "^5.3.0",
|
"react-icons": "^5.3.0",
|
||||||
|
"spawn-sync": "^2.0.0",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|||||||
26
src/app/api/content/[...path]/route.ts
Normal file
26
src/app/api/content/[...path]/route.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import { auth } from "@clerk/nextjs/server";
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: NextRequest,
|
||||||
|
{ params }: { params: { path: string[] } }
|
||||||
|
) {
|
||||||
|
const { userId } = auth();
|
||||||
|
if (!userId) {
|
||||||
|
return new NextResponse('Unauthorized', { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(process.cwd(), 'content', ...params.path);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = await fs.readFile(filePath);
|
||||||
|
const response = new NextResponse(file);
|
||||||
|
response.headers.set('Content-Type', 'application/pdf');
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading file:', error);
|
||||||
|
return new NextResponse('File not found', { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/app/api/forms/[id]/route.ts
Normal file
58
src/app/api/forms/[id]/route.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
import { informedConsentForms, contents } from "~/server/db/schema";
|
||||||
|
import { auth } from "@clerk/nextjs/server";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: { id: string } }
|
||||||
|
) {
|
||||||
|
const { userId } = auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = parseInt(params.id);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
return NextResponse.json({ error: 'Invalid ID' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, get the content associated with this form
|
||||||
|
const [form] = await db
|
||||||
|
.select({
|
||||||
|
contentId: informedConsentForms.contentId,
|
||||||
|
location: contents.location,
|
||||||
|
})
|
||||||
|
.from(informedConsentForms)
|
||||||
|
.innerJoin(contents, eq(informedConsentForms.contentId, contents.id))
|
||||||
|
.where(eq(informedConsentForms.id, id));
|
||||||
|
|
||||||
|
if (!form) {
|
||||||
|
return NextResponse.json({ error: 'Form not found' }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the file from the file system
|
||||||
|
const fullPath = path.join(process.cwd(), form.location);
|
||||||
|
try {
|
||||||
|
await fs.access(fullPath);
|
||||||
|
await fs.unlink(fullPath);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`File not found or couldn't be deleted: ${fullPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the form and content from the database
|
||||||
|
await db.transaction(async (tx) => {
|
||||||
|
await tx.delete(informedConsentForms).where(eq(informedConsentForms.id, id));
|
||||||
|
await tx.delete(contents).where(eq(contents.id, form.contentId));
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ message: "Form deleted successfully" });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting form:', error);
|
||||||
|
return NextResponse.json({ error: 'Failed to delete form' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
104
src/app/api/forms/route.ts
Normal file
104
src/app/api/forms/route.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { db } from "~/server/db";
|
||||||
|
import { contents, informedConsentForms, contentTypes } from "~/server/db/schema";
|
||||||
|
import { auth } from "@clerk/nextjs/server";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { saveFile } from "~/lib/fileStorage";
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
|
// Function to generate a random string
|
||||||
|
const generateRandomString = (length: number) => {
|
||||||
|
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
const { userId } = auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const forms = await db.select({
|
||||||
|
id: informedConsentForms.id,
|
||||||
|
title: contents.title,
|
||||||
|
location: contents.location,
|
||||||
|
previewLocation: contents.previewLocation,
|
||||||
|
studyId: informedConsentForms.studyId,
|
||||||
|
participantId: informedConsentForms.participantId,
|
||||||
|
contentId: informedConsentForms.contentId,
|
||||||
|
}).from(informedConsentForms)
|
||||||
|
.innerJoin(contents, eq(informedConsentForms.contentId, contents.id));
|
||||||
|
|
||||||
|
return NextResponse.json(forms);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const { userId } = auth();
|
||||||
|
if (!userId) {
|
||||||
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
const file = formData.get('file') as File;
|
||||||
|
const title = formData.get('title') as string;
|
||||||
|
const studyId = formData.get('studyId') as string;
|
||||||
|
const participantId = formData.get('participantId') as string;
|
||||||
|
|
||||||
|
if (!file || !title || !studyId || !participantId) {
|
||||||
|
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [formContentType] = await db
|
||||||
|
.select()
|
||||||
|
.from(contentTypes)
|
||||||
|
.where(eq(contentTypes.name, "Informed Consent Form"));
|
||||||
|
|
||||||
|
const [previewContentType] = await db
|
||||||
|
.select()
|
||||||
|
.from(contentTypes)
|
||||||
|
.where(eq(contentTypes.name, "Preview Image"));
|
||||||
|
|
||||||
|
if (!formContentType || !previewContentType) {
|
||||||
|
return NextResponse.json({ error: 'Content type not found' }, { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a random filename with the same extension
|
||||||
|
const fileExtension = file.name.split('.').pop(); // Get the file extension
|
||||||
|
const randomFileName = `${generateRandomString(12)}.${fileExtension}`; // Generate random filename with 12 characters
|
||||||
|
const { pdfPath, previewPath } = await saveFile(file, `${formContentType.id}/${randomFileName}`, previewContentType.id);
|
||||||
|
|
||||||
|
const [content] = await db
|
||||||
|
.insert(contents)
|
||||||
|
.values({
|
||||||
|
contentTypeId: formContentType.id,
|
||||||
|
uploader: userId,
|
||||||
|
location: pdfPath,
|
||||||
|
previewLocation: previewPath,
|
||||||
|
title: title,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
throw new Error("Content not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [form] = await db
|
||||||
|
.insert(informedConsentForms)
|
||||||
|
.values({
|
||||||
|
studyId: parseInt(studyId),
|
||||||
|
participantId: parseInt(participantId),
|
||||||
|
contentId: content.id,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return NextResponse.json(form);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading form:', error);
|
||||||
|
return NextResponse.json({ error: 'Failed to upload form' }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { db } from "~/server/db";
|
|
||||||
import { contents, informedConsentForms, contentTypes } from "~/server/db/schema";
|
|
||||||
import { auth } from "@clerk/nextjs/server";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { saveFile } from '~/lib/fileStorage';
|
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
|
||||||
const { userId } = auth();
|
|
||||||
if (!userId) {
|
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = await request.formData();
|
|
||||||
const file = formData.get('file') as File;
|
|
||||||
const studyId = formData.get('studyId') as string;
|
|
||||||
const participantId = formData.get('participantId') as string;
|
|
||||||
|
|
||||||
if (!file || !studyId || !participantId) {
|
|
||||||
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [contentType] = await db
|
|
||||||
.select()
|
|
||||||
.from(contentTypes)
|
|
||||||
.where(eq(contentTypes.name, "Informed Consent Form"));
|
|
||||||
|
|
||||||
if (!contentType) {
|
|
||||||
return NextResponse.json({ error: 'Content type not found' }, { status: 500 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const [content] = await db
|
|
||||||
.insert(contents)
|
|
||||||
.values({
|
|
||||||
contentTypeId: contentType.id,
|
|
||||||
uploader: userId,
|
|
||||||
location: '', // We'll update this after saving the file
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
throw new Error("Content not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileLocation = await saveFile(file, content.id);
|
|
||||||
|
|
||||||
await db
|
|
||||||
.update(contents)
|
|
||||||
.set({ location: fileLocation })
|
|
||||||
.where(eq(contents.id, content.id));
|
|
||||||
|
|
||||||
const [form] = await db
|
|
||||||
.insert(informedConsentForms)
|
|
||||||
.values({
|
|
||||||
studyId: parseInt(studyId),
|
|
||||||
participantId: parseInt(participantId),
|
|
||||||
contentId: content.id,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
|
|
||||||
return NextResponse.json(form);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading informed consent form:', error);
|
|
||||||
return NextResponse.json({ error: 'Failed to upload form' }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
15
src/app/forms/page.tsx
Normal file
15
src/app/forms/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Layout from "~/components/layout";
|
||||||
|
import { FormsGrid } from "~/components/forms/FormsGrid";
|
||||||
|
import { UploadFormButton } from "~/components/forms/UploadFormButton";
|
||||||
|
|
||||||
|
export default function FormsPage() {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h1 className="text-3xl font-bold">Forms</h1>
|
||||||
|
<UploadFormButton />
|
||||||
|
</div>
|
||||||
|
<FormsGrid />
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useSignIn } from "@clerk/nextjs"
|
import { useSignIn, useSignUp } from "@clerk/nextjs"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "~/components/ui/card"
|
||||||
import { Input } from "~/components/ui/input"
|
import { Input } from "~/components/ui/input"
|
||||||
@@ -12,28 +12,66 @@ import { FcGoogle } from "react-icons/fc"
|
|||||||
import { FaApple } from "react-icons/fa"
|
import { FaApple } from "react-icons/fa"
|
||||||
|
|
||||||
export default function SignInPage() {
|
export default function SignInPage() {
|
||||||
const { isLoaded, signIn, setActive } = useSignIn()
|
const { isLoaded, signIn, setActive } = useSignIn();
|
||||||
const [emailAddress, setEmailAddress] = useState("")
|
const { signUp } = useSignUp();
|
||||||
const [password, setPassword] = useState("")
|
const [emailAddress, setEmailAddress] = useState("");
|
||||||
const router = useRouter()
|
const [password, setPassword] = useState("");
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
if (!isLoaded) return
|
if (!isLoaded) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await signIn.create({
|
const result = await signIn.create({
|
||||||
identifier: emailAddress,
|
identifier: emailAddress,
|
||||||
password,
|
password,
|
||||||
})
|
});
|
||||||
|
|
||||||
if (result.status === "complete") {
|
if (result.status === "complete") {
|
||||||
await setActive({ session: result.createdSessionId })
|
await setActive({ session: result.createdSessionId });
|
||||||
router.push("/dash")
|
router.push("/dash");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as { errors?: { message: string }[] };
|
const error = err as { errors?: { message: string }[] };
|
||||||
console.error("Error:", error.errors?.[0]?.message ?? "Unknown error")
|
console.error("Error:", error.errors?.[0]?.message ?? "Unknown error");
|
||||||
|
|
||||||
|
// If the error indicates the user does not exist, trigger sign-up
|
||||||
|
if (error.errors?.[0]?.message.includes("not found")) {
|
||||||
|
if (!signUp) {
|
||||||
|
console.error("Sign-up functionality is not available.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const signUpResult = await signUp.create({
|
||||||
|
emailAddress,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (signUpResult.status === "complete") {
|
||||||
|
await setActive({ session: signUpResult.createdSessionId });
|
||||||
|
|
||||||
|
// Create a user entry in the database
|
||||||
|
const response = await fetch('/api/users', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email: emailAddress }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error("Error creating user in database:", errorData.error);
|
||||||
|
return; // Optionally handle the error (e.g., show a message to the user)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push("/dash");
|
||||||
|
}
|
||||||
|
} catch (signUpErr) {
|
||||||
|
const signUpError = signUpErr as { errors?: { message: string }[] };
|
||||||
|
console.error("Sign-up Error:", signUpError.errors?.[0]?.message ?? "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +90,7 @@ export default function SignInPage() {
|
|||||||
<div className="min-h-screen bg-gradient-to-b from-blue-100 to-white flex items-center justify-center">
|
<div className="min-h-screen bg-gradient-to-b from-blue-100 to-white flex items-center justify-center">
|
||||||
<Card className="w-[350px]">
|
<Card className="w-[350px]">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sign In to HRIStudio</CardTitle>
|
<CardTitle>Sign in to HRIStudio</CardTitle>
|
||||||
<CardDescription>Enter your email and password to sign in</CardDescription>
|
<CardDescription>Enter your email and password to sign in</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export default function SignUpPage() {
|
|||||||
<div className="min-h-screen bg-gradient-to-b from-blue-100 to-white flex items-center justify-center">
|
<div className="min-h-screen bg-gradient-to-b from-blue-100 to-white flex items-center justify-center">
|
||||||
<Card className="w-[350px]">
|
<Card className="w-[350px]">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Sign Up for HRIStudio</CardTitle>
|
<CardTitle>Sign up for HRIStudio</CardTitle>
|
||||||
<CardDescription>Create an account to get started</CardDescription>
|
<CardDescription>Create an account to get started</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
48
src/components/forms/FormCard.tsx
Normal file
48
src/components/forms/FormCard.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Card, CardContent, CardFooter } from "~/components/ui/card";
|
||||||
|
import { Badge } from "~/components/ui/badge";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
|
||||||
|
interface FormCardProps {
|
||||||
|
form: {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
location: string;
|
||||||
|
studyId: number;
|
||||||
|
studyTitle: string;
|
||||||
|
participantId: number;
|
||||||
|
participantName: string;
|
||||||
|
previewLocation: string; // Added this property
|
||||||
|
};
|
||||||
|
onDelete: (formId: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormCard({ form, onDelete }: FormCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<img
|
||||||
|
src={form.previewLocation}
|
||||||
|
alt={form.title}
|
||||||
|
className="w-full h-40 object-cover"
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex flex-col items-start p-4">
|
||||||
|
<h3 className="font-semibold mb-2">{form.title}</h3>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
<Badge variant="secondary">{form.studyTitle}</Badge>
|
||||||
|
<Badge variant="outline">{form.participantName}</Badge>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-destructive"
|
||||||
|
onClick={() => onDelete(form.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
src/components/forms/FormsGrid.tsx
Normal file
74
src/components/forms/FormsGrid.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { FormCard } from "~/components/forms/FormCard";
|
||||||
|
import { useToast } from "~/hooks/use-toast";
|
||||||
|
|
||||||
|
interface Form {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
location: string;
|
||||||
|
studyId: number;
|
||||||
|
studyTitle: string;
|
||||||
|
participantId: number;
|
||||||
|
participantName: string;
|
||||||
|
previewLocation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormsGrid() {
|
||||||
|
const [forms, setForms] = useState<Form[]>([]);
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchForms();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchForms = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/forms");
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch forms");
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setForms(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching forms:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to load forms. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (formId: number) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/forms/${formId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to delete form");
|
||||||
|
}
|
||||||
|
setForms(forms.filter((form) => form.id !== formId));
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Form deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting form:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to delete form. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
|
||||||
|
{forms.map((form) => (
|
||||||
|
<FormCard key={form.id} form={form} onDelete={handleDelete} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/components/forms/UploadFormButton.tsx
Normal file
113
src/components/forms/UploadFormButton.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
import { Label } from "~/components/ui/label";
|
||||||
|
import { useToast } from "~/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
} from "~/components/ui/dialog";
|
||||||
|
import { useStudyContext } from "~/context/StudyContext";
|
||||||
|
|
||||||
|
export function UploadFormButton() {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [participantId, setParticipantId] = useState("");
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { selectedStudy } = useStudyContext();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!file || !title || !participantId || !selectedStudy) {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Please fill in all fields and select a file.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("file", file);
|
||||||
|
formData.append("title", title);
|
||||||
|
formData.append("studyId", selectedStudy.id.toString());
|
||||||
|
formData.append("participantId", participantId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/forms", {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to upload form");
|
||||||
|
}
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Form uploaded successfully",
|
||||||
|
});
|
||||||
|
setIsOpen(false);
|
||||||
|
setFile(null);
|
||||||
|
setTitle("");
|
||||||
|
setParticipantId("");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error uploading form:", error);
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: "Failed to upload form. Please try again.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button>Upload Form</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Upload New Form</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="title">Title</Label>
|
||||||
|
<Input
|
||||||
|
id="title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="participantId">Participant ID</Label>
|
||||||
|
<Input
|
||||||
|
id="participantId"
|
||||||
|
value={participantId}
|
||||||
|
onChange={(e) => setParticipantId(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="file">File</Label>
|
||||||
|
<Input
|
||||||
|
id="file"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf"
|
||||||
|
onChange={(e) => setFile(e.target.files?.[0] || null)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button type="submit">Upload</Button>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/components/participant/ParticipantCard.tsx
Normal file
30
src/components/participant/ParticipantCard.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Card, CardContent, CardFooter } from "~/components/ui/card";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
import { Participant } from "../../types/Participant";
|
||||||
|
|
||||||
|
interface ParticipantCardProps {
|
||||||
|
participant: Participant;
|
||||||
|
onDelete: (participantId: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ParticipantCard({ participant, onDelete }: ParticipantCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className="overflow-hidden">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<h3 className="font-semibold mb-2">{participant.name}</h3>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-end p-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-destructive"
|
||||||
|
onClick={() => onDelete(participant.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,9 +6,8 @@ import { Button } from "~/components/ui/button";
|
|||||||
import { useStudyContext } from '../../context/StudyContext';
|
import { useStudyContext } from '../../context/StudyContext';
|
||||||
import { Participant } from '../../types/Participant';
|
import { Participant } from '../../types/Participant';
|
||||||
import { CreateParticipantDialog } from './CreateParticipantDialog';
|
import { CreateParticipantDialog } from './CreateParticipantDialog';
|
||||||
import { Trash2 } from 'lucide-react';
|
|
||||||
import { useToast } from '~/hooks/use-toast';
|
import { useToast } from '~/hooks/use-toast';
|
||||||
import { UploadConsentForm } from './UploadConsentForm';
|
import { ParticipantCard } from './ParticipantCard';
|
||||||
|
|
||||||
export function Participants() {
|
export function Participants() {
|
||||||
const [participants, setParticipants] = useState<Participant[]>([]);
|
const [participants, setParticipants] = useState<Participant[]>([]);
|
||||||
@@ -89,24 +88,11 @@ export function Participants() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{participants.length > 0 ? (
|
{participants.length > 0 ? (
|
||||||
<ul className="space-y-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-4">
|
||||||
{participants.map(participant => (
|
{participants.map(participant => (
|
||||||
<li key={participant.id} className="bg-gray-100 p-4 rounded">
|
<ParticipantCard key={participant.id} participant={participant} onDelete={deleteParticipant} />
|
||||||
<div className="flex justify-between items-center mb-2">
|
|
||||||
<span className="font-semibold">{participant.name}</span>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => deleteParticipant(participant.id)}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<UploadConsentForm studyId={selectedStudy.id} participantId={participant.id} />
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<p>No participants added yet.</p>
|
<p>No participants added yet.</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
|
||||||
import { Button } from "~/components/ui/button";
|
|
||||||
import { Input } from "~/components/ui/input";
|
|
||||||
import { useToast } from "~/hooks/use-toast";
|
|
||||||
|
|
||||||
interface UploadConsentFormProps {
|
|
||||||
studyId: number;
|
|
||||||
participantId: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function UploadConsentForm({ studyId, participantId }: UploadConsentFormProps) {
|
|
||||||
const [file, setFile] = useState<File | null>(null);
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
toast({
|
|
||||||
title: "Test Toast",
|
|
||||||
description: "This is a test toast message",
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (e.target.files && e.target.files[0]) {
|
|
||||||
setFile(e.target.files[0]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!file) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Please select a file to upload",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsUploading(true);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
formData.append('studyId', studyId.toString());
|
|
||||||
formData.append('participantId', participantId.toString());
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/informed-consent', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to upload form');
|
|
||||||
}
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Informed consent form uploaded successfully",
|
|
||||||
});
|
|
||||||
setFile(null);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading form:', error);
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: "Failed to upload informed consent form",
|
|
||||||
variant: "destructive",
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsUploading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
type="file"
|
|
||||||
accept=".pdf"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
disabled={isUploading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Button type="submit" disabled={!file || isUploading}>
|
|
||||||
{isUploading ? 'Uploading...' : 'Upload Consent Form'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
BeakerIcon,
|
BeakerIcon,
|
||||||
BotIcon,
|
BotIcon,
|
||||||
FolderIcon,
|
FolderIcon,
|
||||||
|
FileTextIcon,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Menu,
|
Menu,
|
||||||
Settings
|
Settings
|
||||||
@@ -22,6 +23,7 @@ const navItems = [
|
|||||||
{ name: "Dashboard", href: "/dash", icon: LayoutDashboard },
|
{ name: "Dashboard", href: "/dash", icon: LayoutDashboard },
|
||||||
{ name: "Studies", href: "/studies", icon: FolderIcon },
|
{ name: "Studies", href: "/studies", icon: FolderIcon },
|
||||||
{ name: "Participants", href: "/participants", icon: BeakerIcon },
|
{ name: "Participants", href: "/participants", icon: BeakerIcon },
|
||||||
|
{ name: "Forms", href: "/forms", icon: FileTextIcon },
|
||||||
{ name: "Data Analysis", href: "/analysis", icon: BarChartIcon },
|
{ name: "Data Analysis", href: "/analysis", icon: BarChartIcon },
|
||||||
{ name: "Settings", href: "/settings", icon: Settings },
|
{ name: "Settings", href: "/settings", icon: Settings },
|
||||||
];
|
];
|
||||||
|
|||||||
36
src/components/ui/badge.tsx
Normal file
36
src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "~/lib/utils"
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"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",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
@@ -1,15 +1,38 @@
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { fromBuffer } from 'pdf2pic';
|
||||||
|
|
||||||
const CONTENT_DIR = path.join(process.cwd(), 'content');
|
const CONTENT_DIR = path.join(process.cwd(), 'content');
|
||||||
|
|
||||||
export async function saveFile(file: File, contentId: number): Promise<string> {
|
export async function saveFile(file: File, filePath: string, previewContentTypeId: number): Promise<{ pdfPath: string; previewPath: string }> {
|
||||||
const contentFolder = path.join(CONTENT_DIR, contentId.toString());
|
const fullPath = path.join(CONTENT_DIR, filePath);
|
||||||
await fs.mkdir(contentFolder, { recursive: true });
|
const dir = path.dirname(fullPath);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
const buffer = Buffer.from(await file.arrayBuffer());
|
const buffer = Buffer.from(await file.arrayBuffer());
|
||||||
const filePath = path.join(contentFolder, file.name);
|
await fs.writeFile(fullPath, buffer);
|
||||||
await fs.writeFile(filePath, buffer);
|
|
||||||
|
|
||||||
return filePath;
|
// Generate preview image
|
||||||
|
const previewFileName = path.basename(filePath, '.pdf') + '_preview.png';
|
||||||
|
const previewDir = path.join(CONTENT_DIR, previewContentTypeId.toString());
|
||||||
|
await fs.mkdir(previewDir, { recursive: true });
|
||||||
|
const previewPath = path.join(previewDir, previewFileName);
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
density: 100,
|
||||||
|
saveFilename: path.basename(previewPath, '.png'),
|
||||||
|
savePath: previewDir,
|
||||||
|
format: "png",
|
||||||
|
width: 600,
|
||||||
|
height: 800
|
||||||
|
};
|
||||||
|
|
||||||
|
const convert = fromBuffer(buffer, options);
|
||||||
|
await convert(1);
|
||||||
|
|
||||||
|
// Return relative paths that can be used in URLs
|
||||||
|
return {
|
||||||
|
pdfPath: `/content/${filePath}`,
|
||||||
|
previewPath: `/content/${previewContentTypeId}/${previewFileName}`
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,9 @@ export async function initializeContentTypes() {
|
|||||||
if (existingTypes.length === 0) {
|
if (existingTypes.length === 0) {
|
||||||
await db.insert(contentTypes).values([
|
await db.insert(contentTypes).values([
|
||||||
{ name: "Informed Consent Form" },
|
{ name: "Informed Consent Form" },
|
||||||
|
{ name: "Preview Image" }, // New content type
|
||||||
// Add other content types as needed
|
// Add other content types as needed
|
||||||
]);
|
]);
|
||||||
console.log("Content types initialized");
|
console.log("Content types initialized");
|
||||||
} else {
|
|
||||||
console.log("Content types already exist");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
// Example model schema from the Drizzle docs
|
// Example model schema from the Drizzle docs
|
||||||
// https://orm.drizzle.team/docs/sql-schema-declaration
|
// https://orm.drizzle.team/docs/sql-schema-declaration
|
||||||
|
|
||||||
import { pgTableCreator } from "drizzle-orm/pg-core";
|
import { pgTable } from "drizzle-orm/pg-core";
|
||||||
import {
|
import {
|
||||||
serial,
|
serial,
|
||||||
varchar,
|
varchar,
|
||||||
timestamp,
|
timestamp,
|
||||||
integer,
|
integer
|
||||||
pgTable
|
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
|
|
||||||
@@ -17,9 +16,7 @@ import { sql } from "drizzle-orm";
|
|||||||
*
|
*
|
||||||
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
||||||
*/
|
*/
|
||||||
export const createTable = pgTableCreator((name) => `hristudio_${name}`);
|
export const studies = pgTable(
|
||||||
|
|
||||||
export const studies = createTable(
|
|
||||||
"study",
|
"study",
|
||||||
{
|
{
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
@@ -35,7 +32,7 @@ export const studies = createTable(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const participants = createTable(
|
export const participants = pgTable(
|
||||||
"participant",
|
"participant",
|
||||||
{
|
{
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
@@ -47,7 +44,7 @@ export const participants = createTable(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const contentTypes = createTable(
|
export const contentTypes = pgTable(
|
||||||
"content_type",
|
"content_type",
|
||||||
{
|
{
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
@@ -55,20 +52,22 @@ export const contentTypes = createTable(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const contents = createTable(
|
export const contents = pgTable(
|
||||||
"content",
|
"content",
|
||||||
{
|
{
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
contentTypeId: integer("content_type_id").references(() => contentTypes.id).notNull(),
|
contentTypeId: integer("content_type_id").references(() => contentTypes.id).notNull(),
|
||||||
uploader: varchar("uploader", { length: 256 }).notNull(),
|
uploader: varchar("uploader", { length: 256 }).notNull(),
|
||||||
location: varchar("location", { length: 1000 }).notNull(),
|
location: varchar("location", { length: 1000 }).notNull(),
|
||||||
|
previewLocation: varchar("preview_location", { length: 1000 }),
|
||||||
|
title: varchar("title", { length: 256 }).notNull(),
|
||||||
createdAt: timestamp("created_at", { withTimezone: true })
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
.default(sql`CURRENT_TIMESTAMP`)
|
.default(sql`CURRENT_TIMESTAMP`)
|
||||||
.notNull(),
|
.notNull(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const informedConsentForms = createTable(
|
export const informedConsentForms = pgTable(
|
||||||
"informed_consent_form",
|
"informed_consent_form",
|
||||||
{
|
{
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
@@ -80,3 +79,14 @@ export const informedConsentForms = createTable(
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const users = pgTable(
|
||||||
|
"user",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
email: varchar("email", { length: 256 }).notNull().unique(),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.default(sql`CURRENT_TIMESTAMP`)
|
||||||
|
.notNull(),
|
||||||
|
}
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user