diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 8fa44b5..5a2fa89 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,5 +1,5 @@ 'use client'; -import { contactFormSubmit, testMailerSDK } from "@/server/actions/mailer.actions" +import { testMailerSDK } from "@/server/actions/mailer.actions" import { useMemo, useState } from "react"; export default function ContactPage() { diff --git a/app/links/page.tsx b/app/links/page.tsx index 28e6737..967b246 100644 --- a/app/links/page.tsx +++ b/app/links/page.tsx @@ -1,10 +1,7 @@ import SocialMedia from "@/components/Links/SocialMedia"; -import Panel from "@/components/ui/Panel"; -import Image from "next/image"; -import { FaBandcamp, FaGithub, FaLinkedin, FaPatreon, FaSoundcloud, FaYoutube } from "react-icons/fa"; -import { RxArrowRight } from "react-icons/rx"; +import { FaBandcamp, FaGithub, FaLinkedin, FaSoundcloud } from "react-icons/fa"; -export default function LinksPage() { +export default async function LinksPage() { return (
{/*
diff --git a/app/page.tsx b/app/page.tsx index f92f24b..3b20f04 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,7 +2,7 @@ import { ColorChangeName } from "@/components/Home"; import Image from "next/image"; import Link from "next/link"; -export default function Home() { +export default async function Home() { return (
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 not found!

+ } return ( -
-

ProjectById Page

-

Project ID: {pathname}

-
+
+
+

{project.name}

+ {/*

Started: {project.startDate.toLocaleString()}

+

{project.endDate ? `Finished: ${project.endDate.toLocaleDateString()}` : "(In progress)"}

*/} +
+ +
+

{project.description}

+
+ + { project.media && project.media.map((link, idx) => { + return {`Media + })} +
) } 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 {`Media + })} +
+ ) +} 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); -}