diff --git a/.env b/.env
index 87b0984..1603226 100644
--- a/.env
+++ b/.env
@@ -5,8 +5,8 @@
DATABASE_URL="postgresql://postgres:jusxah-jufrew-niwjY5@db:5432/hristudio"
# Clerk
-NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_YWxsb3dlZC1zYWxtb24tNjMuY2xlcmsuYWNjb3VudHMuZGV2JA
-CLERK_SECRET_KEY=sk_test_nUKl0GTM5ibgUH12WbTH6pNVHAyRshlSFi64IrEeWD
+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
diff --git a/Dockerfile b/Dockerfile
index 72366a4..23ac3d7 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,8 @@
# 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
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 . .
-# Push database schema to database
-RUN pnpm drizzle-kit push
+# # Clear previous build artifacts
+# 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
CMD ["pnpm", "run", "dev"]
\ No newline at end of file
diff --git a/drizzle.config.ts b/drizzle.config.ts
index b4164b7..33b6656 100644
--- a/drizzle.config.ts
+++ b/drizzle.config.ts
@@ -8,5 +8,5 @@ export default {
dbCredentials: {
url: env.DATABASE_URL,
},
- tablesFilter: ["hristudio_*"],
+ // tablesFilter: ["hristudio_*"],
} satisfies Config;
diff --git a/next.config.js b/next.config.js
index 9bfe4a0..1c81fee 100644
--- a/next.config.js
+++ b/next.config.js
@@ -4,7 +4,38 @@
*/
await import("./src/env.js");
-/** @type {import("next").NextConfig} */
-const config = {};
+/** @type {import('next').NextConfig} */
+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;
diff --git a/package.json b/package.json
index ddfc4cb..976b641 100644
--- a/package.json
+++ b/package.json
@@ -34,11 +34,13 @@
"lucide-react": "^0.441.0",
"next": "^14.2.12",
"next-themes": "^0.3.0",
+ "pdf2pic": "^3.1.3",
"postgres": "^3.4.4",
"radix-ui": "^1.0.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
+ "spawn-sync": "^2.0.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.23.8"
diff --git a/src/app/api/content/[...path]/route.ts b/src/app/api/content/[...path]/route.ts
new file mode 100644
index 0000000..08bb858
--- /dev/null
+++ b/src/app/api/content/[...path]/route.ts
@@ -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 });
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/forms/[id]/route.ts b/src/app/api/forms/[id]/route.ts
new file mode 100644
index 0000000..cefe0f4
--- /dev/null
+++ b/src/app/api/forms/[id]/route.ts
@@ -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 });
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/forms/route.ts b/src/app/api/forms/route.ts
new file mode 100644
index 0000000..6a778ce
--- /dev/null
+++ b/src/app/api/forms/route.ts
@@ -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 });
+ }
+}
\ No newline at end of file
diff --git a/src/app/api/informed-consent/route.ts b/src/app/api/informed-consent/route.ts
deleted file mode 100644
index a0ef527..0000000
--- a/src/app/api/informed-consent/route.ts
+++ /dev/null
@@ -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 });
- }
-}
\ No newline at end of file
diff --git a/src/app/forms/page.tsx b/src/app/forms/page.tsx
new file mode 100644
index 0000000..fc28550
--- /dev/null
+++ b/src/app/forms/page.tsx
@@ -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 (
+ Forms
+