diff --git a/app/projects/[id]/page.tsx b/app/projects/[id]/page.tsx
index d474e83..27ad7a3 100644
--- a/app/projects/[id]/page.tsx
+++ b/app/projects/[id]/page.tsx
@@ -1,12 +1,39 @@
-import { usePathname } from 'next/navigation'
+import Image from "next/image";
+import ProjectRepository from "@/server/actions/project.actions";
-export default function ProjectById() {
- const pathname = usePathname();
+interface PageProps {
+ searchParams: any;
+ params: {
+ id: any;
+ }
+ children: React.ReactNode;
+}
+
+export default async function ProjectById(props: PageProps) {
+ const { id } = props.params;
+
+ const projects = new ProjectRepository();
+ const project = await projects.getProjectById(id);
+
+ if (!project) {
+ return
+
+ {project.name}
+ {/* Started: {project.startDate.toLocaleString()}
+ {project.endDate ? `Finished: ${project.endDate.toLocaleDateString()}` : "(In progress)"}
*/}
+
+
+
+
{project.description}
+
+
+ { project.media && project.media.map((link, idx) => {
+ return
+ })}
+
)
}
diff --git a/app/projects/page.tsx b/app/projects/page.tsx
index 637831b..7c11e9d 100644
--- a/app/projects/page.tsx
+++ b/app/projects/page.tsx
@@ -1,4 +1,4 @@
-export default function ProjectsPage() {
+export default async function ProjectsPage() {
return (
Learn more about my work
diff --git a/components/Home/index.tsx b/components/Home/index.tsx
index eda5a5b..d35c4c9 100644
--- a/components/Home/index.tsx
+++ b/components/Home/index.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useColorShift } from "../logo";
+import { useColorShift } from "../Navbar/logo";
export const ColorChangeName = () => {
const { firstColor, secondColor, thirdColor } = useColorShift(14000);
diff --git a/components/Navbar/index.tsx b/components/Navbar/index.tsx
index 1696ac3..4536f5b 100644
--- a/components/Navbar/index.tsx
+++ b/components/Navbar/index.tsx
@@ -1,5 +1,5 @@
import Link from 'next/link'
-import { InlineLogo, useColorShift } from '../logo'
+import { InlineLogo, useColorShift } from './logo'
import { useState } from 'react';
import { RxActivityLog } from "react-icons/rx";
import { NavbarButton } from '../ui/Button';
diff --git a/components/logo/index.tsx b/components/Navbar/logo.tsx
similarity index 97%
rename from components/logo/index.tsx
rename to components/Navbar/logo.tsx
index f444eda..51f5b3d 100644
--- a/components/logo/index.tsx
+++ b/components/Navbar/logo.tsx
@@ -1,8 +1,8 @@
'use client'
import { FC } from "react";
-import useColorShift, { UseColorShiftReturnType, type ColorListType } from "./useColorShift";
+import useColorShift, { UseColorShiftReturnType, type ColorListType } from "../../hooks/useColorShift";
import { useRouter } from "next/navigation";
-export { default as useColorShift } from "./useColorShift";
+export { default as useColorShift } from "../../hooks/useColorShift";
const DEFAULT_SHIFT_INTERVAL = 3000;
diff --git a/components/Projects/ProjectEntry.tsx b/components/Projects/ProjectEntry.tsx
new file mode 100644
index 0000000..d6be635
--- /dev/null
+++ b/components/Projects/ProjectEntry.tsx
@@ -0,0 +1,29 @@
+import ProjectRepository from "@/server/actions/project.actions"
+import Image from "next/image"
+
+export default async function ProjectEntry({ id }: { id: string }) {
+ const projects = new ProjectRepository();
+ const project = await projects.getProjectById(id);
+
+ if (!project) {
+ return
Project not found!
+ }
+
+ return (
+
+
+ {project.name}
+ Started: {project.startDate.toLocaleString()}
+ {project.endDate ? `Finished: ${project.endDate.toLocaleDateString()}` : "(In progress)"}
+
+
+
+
{project.description}
+
+
+ { project.media && project.media.map((link, idx) => {
+ return
+ })}
+
+ )
+}
diff --git a/components/mdx/index.tsx b/components/mdx/index.tsx
deleted file mode 100644
index 0801caa..0000000
--- a/components/mdx/index.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { v4 } from "uuid"
-import { cabin } from "@/app/layout";
-
-type ElementType
= React.FC
-type FormattedTags = {
- [Key in keyof JSX.IntrinsicElements]: ElementType
-}
-
-const H1TAG: ElementType<"h1"> = ({ children }) => { return (
- {children}
-)}
-
-const H2Tag: ElementType<"h2"> = ({ children }) => (
- {children}
-)
-
-const H3Tag: ElementType<"h3"> = ({ children }) => (
- {children}
-)
-
-const H4Tag: ElementType<"h4"> = ({ children }) => (
- {children}
-)
-
-const PTag: ElementType<"p"> = ({ children }) => (
- {children}
-)
-
-const LiTag: ElementType<"li"> = ({ children }) => (
- {children}
-)
-
-const BrTag: ElementType<"br"> = () => (
-
-)
-
-export default {
- "h1": H1TAG,
- "h2": H2Tag,
- "h3": H3Tag,
- "h4": H4Tag,
- "p": PTag,
- "li": LiTag,
- "br": BrTag
-} satisfies Partial
diff --git a/components/logo/useColorShift.tsx b/hooks/useColorShift.tsx
similarity index 100%
rename from components/logo/useColorShift.tsx
rename to hooks/useColorShift.tsx
diff --git a/package.json b/package.json
index b0f1db6..8a04b11 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,11 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.367.0",
"@sendgrid/mail": "^7.7.0",
- "@supabase/supabase-js": "^2.26.0",
"autoprefixer": "10.4.14",
"eslint": "^8.46.0",
"eslint-config-next": "^13.4.12",
"next": "^13.4.12",
+ "pg": "^8.11.3",
"postcss": "8.4.24",
"react": "18.2.0",
"react-dom": "18.2.0",
@@ -26,9 +26,11 @@
},
"devDependencies": {
"@types/node": "20.2.5",
+ "@types/pg": "^8.10.3",
"@types/react": "18.2.7",
"@types/react-dom": "18.2.4",
"@types/uuid": "^9.0.1",
- "@vercel/style-guide": "^5.0.1"
+ "@vercel/style-guide": "^5.0.1",
+ "zod": "^3.22.4"
}
}
diff --git a/server/actions/project.actions.ts b/server/actions/project.actions.ts
new file mode 100644
index 0000000..37273e2
--- /dev/null
+++ b/server/actions/project.actions.ts
@@ -0,0 +1,34 @@
+import { Project, isProject } from "../entities/project";
+import { Client } from "pg";
+import createClient from "../services/pg";
+
+export default class ProjectRepository {
+ async getProjects() {
+ 'use server';
+
+ const client = createClient();
+
+ const { rows } = await client.query("SELECT * FROM projects");
+ await client.end();
+
+ if (rows.every(row => isProject(row))) {
+ return rows as Project[];
+ }
+
+ return null;
+ }
+
+ async getProjectById(id: string) {
+ 'use server';
+
+ const client = createClient();
+ const { rows } = await client.query("SELECT * FROM projects WHERE id = $1", [id]);
+ await client.end();
+
+ if (rows.every(row => isProject(row))) {
+ return rows[0] as Project;
+ }
+
+ return null;
+ }
+}
diff --git a/server/actions/projects.actions.ts b/server/actions/projects.actions.ts
deleted file mode 100644
index 894c244..0000000
--- a/server/actions/projects.actions.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import supabaseClient from "../services/supabase";
-
-export default class ProjectsActions {
- static api = supabaseClient();
-
- static async getProjects() {
- const { data, error } = await this.api.from("projects").select("*");
-
- if (error) throw error;
- return data;
- }
-
- static async getProjectsById(id: string) {
- const { data, error } = await this.api.from("projects").select("*").eq("id", id);
-
- if (error) throw error;
- return data;
- }
-}
diff --git a/server/actions/work.actions.ts b/server/actions/work.actions.ts
index 86d153b..bbbf251 100644
--- a/server/actions/work.actions.ts
+++ b/server/actions/work.actions.ts
@@ -1,21 +1,15 @@
-'use server';
-
-import supabaseClient from "../services/supabase";
-
export default class WorkActions {
- static api = supabaseClient();
+ // static async getWork() {
+ // const { data, error } = await this.api.from("work").select("*");
- static async getWork() {
- const { data, error } = await this.api.from("work").select("*");
+ // if (error) throw error;
+ // return data;
+ // }
- if (error) throw error;
- return data;
- }
+ // static async getWorkById(id: string) {
+ // const { data, error } = await this.api.from("work").select("*").eq("id", id);
- static async getWorkById(id: string) {
- const { data, error } = await this.api.from("work").select("*").eq("id", id);
-
- if (error) throw error;
- return data;
- }
+ // if (error) throw error;
+ // return data;
+ // }
}
diff --git a/server/entities/project.ts b/server/entities/project.ts
index 270f5b6..3f91f37 100644
--- a/server/entities/project.ts
+++ b/server/entities/project.ts
@@ -1,9 +1,48 @@
-type Project = {
- name: string;
- startDate: Date;
- endDate: Date;
- current: boolean;
- description: string;
- tagIDs: string[];
- media?: string[]; // array of URLs
+import { z } from "zod";
+
+export type Project = {
+ name: string;
+ description: string;
+ startDate: Date;
+ endDate?: Date;
+ tagIDs?: string[];
+ media?: string[]; // array of URLs
+}
+
+export class ProjectCreationError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "ProjectCreationError";
+ }
+}
+
+export const ZProject = z.object({
+ name: z.string(),
+ description: z.string(),
+ startDate: z.date(),
+ endDate: z.date().optional(),
+ tagIDs: z.array(z.string()).optional(),
+ media: z.array(z.string()).optional(),
+})
+
+export function createProject(data: Partial) {
+ if (!data.name) throw new ProjectCreationError("Project name is required");
+ if (!data.description) throw new ProjectCreationError("Project description is required");
+
+ const today = new Date();
+
+ const completeInput = {
+ name: data.name,
+ description: data.description,
+ startDate: data.startDate || today,
+ }
+
+ const parsedProject = ZProject.safeParse(completeInput);
+
+ if (!parsedProject.success) throw new ProjectCreationError("Invalid project data");
+ return parsedProject.data satisfies Project;
+}
+
+export function isProject(data: unknown): data is Project {
+ return ZProject.safeParse(data).success;
}
diff --git a/server/services/pg.ts b/server/services/pg.ts
new file mode 100644
index 0000000..cf2d653
--- /dev/null
+++ b/server/services/pg.ts
@@ -0,0 +1,19 @@
+import { Client } from "pg";
+
+export class PostgresError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "PostgresError";
+ }
+}
+
+export default function createClient() {
+ if (!process.env.POSTGRES_URL) {
+ throw new PostgresError("Database connection configured incorrectly")
+ }
+
+ return new Client({
+ connectionString: process.env.POSTGRES_URL
+ });
+
+}
diff --git a/server/services/supabase.ts b/server/services/supabase.ts
deleted file mode 100644
index f40935b..0000000
--- a/server/services/supabase.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { createClient } from "@supabase/supabase-js";
-
-export default function supabaseClient() {
- if (typeof process.env.SUPABASE_URL !== "string") {
- throw new Error("SUPABASE_URL is not defined");
- }
-
- if (typeof process.env.SUPABASE_KEY !== "string") {
- throw new Error("SUPABASE_KEY is not defined");
- }
-
- return createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY);
-}