in progress: build out server side

This commit is contained in:
2023-10-06 15:13:57 -05:00
parent 111e211fa7
commit a3ff7598ae
18 changed files with 186 additions and 122 deletions

View File

@@ -1,5 +1,5 @@
'use client'; 'use client';
import { contactFormSubmit, testMailerSDK } from "@/server/actions/mailer.actions" import { testMailerSDK } from "@/server/actions/mailer.actions"
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
export default function ContactPage() { export default function ContactPage() {

View File

@@ -1,10 +1,7 @@
import SocialMedia from "@/components/Links/SocialMedia"; import SocialMedia from "@/components/Links/SocialMedia";
import Panel from "@/components/ui/Panel"; import { FaBandcamp, FaGithub, FaLinkedin, FaSoundcloud } from "react-icons/fa";
import Image from "next/image";
import { FaBandcamp, FaGithub, FaLinkedin, FaPatreon, FaSoundcloud, FaYoutube } from "react-icons/fa";
import { RxArrowRight } from "react-icons/rx";
export default function LinksPage() { export default async function LinksPage() {
return ( return (
<div className="flex flex-col min-h-screen min-w-screen items-center bg-gradient-to-b from-slate-300 to-sky-100 dark:from-black dark:to-slate-900 bg-fixed"> <div className="flex flex-col min-h-screen min-w-screen items-center bg-gradient-to-b from-slate-300 to-sky-100 dark:from-black dark:to-slate-900 bg-fixed">
{/* <section className="flex flex-col flex-wrap w-11/12 m-12"> {/* <section className="flex flex-col flex-wrap w-11/12 m-12">

View File

@@ -2,7 +2,7 @@ import { ColorChangeName } from "@/components/Home";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
export default function Home() { export default async function Home() {
return ( return (
<main className="min-h-screen bg-fixed bg-gradient-to-b from-slate-300 to-slate-400 dark:from-black dark:to-slate-900"> <main className="min-h-screen bg-fixed bg-gradient-to-b from-slate-300 to-slate-400 dark:from-black dark:to-slate-900">
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">

View File

@@ -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() { interface PageProps {
const pathname = usePathname(); 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 <p>Project not found!</p>
}
return ( return (
<div> <article id="project-entry-body">
<h1>ProjectById Page</h1> <header>
<p>Project ID: {pathname}</p> <h1 className="text-4xl font-bold">{project.name}</h1>
</div> {/* <p>Started: {project.startDate.toLocaleString()}</p>
<p>{project.endDate ? `Finished: ${project.endDate.toLocaleDateString()}` : "(In progress)"}</p> */}
</header>
<div id="project-entry-content" className="flex flex-col">
<p>{project.description}</p>
</div>
{ project.media && project.media.map((link, idx) => {
return <Image src={link} key={idx} alt={`Media for ${project.name}`} width={80} height={80} />
})}
</article>
) )
} }

View File

@@ -1,4 +1,4 @@
export default function ProjectsPage() { export default async function ProjectsPage() {
return ( return (
<div> <div>
<h1>Learn more about my work</h1> <h1>Learn more about my work</h1>

View File

@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useColorShift } from "../logo"; import { useColorShift } from "../Navbar/logo";
export const ColorChangeName = () => { export const ColorChangeName = () => {
const { firstColor, secondColor, thirdColor } = useColorShift(14000); const { firstColor, secondColor, thirdColor } = useColorShift(14000);

View File

@@ -1,5 +1,5 @@
import Link from 'next/link' import Link from 'next/link'
import { InlineLogo, useColorShift } from '../logo' import { InlineLogo, useColorShift } from './logo'
import { useState } from 'react'; import { useState } from 'react';
import { RxActivityLog } from "react-icons/rx"; import { RxActivityLog } from "react-icons/rx";
import { NavbarButton } from '../ui/Button'; import { NavbarButton } from '../ui/Button';

View File

@@ -1,8 +1,8 @@
'use client' 'use client'
import { FC } from "react"; import { FC } from "react";
import useColorShift, { UseColorShiftReturnType, type ColorListType } from "./useColorShift"; import useColorShift, { UseColorShiftReturnType, type ColorListType } from "../../hooks/useColorShift";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
export { default as useColorShift } from "./useColorShift"; export { default as useColorShift } from "../../hooks/useColorShift";
const DEFAULT_SHIFT_INTERVAL = 3000; const DEFAULT_SHIFT_INTERVAL = 3000;

View File

@@ -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 <p>Project not found!</p>
}
return (
<article id="project-entry-body">
<header>
<h1 className="text-4xl font-bold">{project.name}</h1>
<p>Started: {project.startDate.toLocaleString()}</p>
<p>{project.endDate ? `Finished: ${project.endDate.toLocaleDateString()}` : "(In progress)"}</p>
</header>
<div id="project-entry-content" className="flex flex-col">
<p>{project.description}</p>
</div>
{ project.media && project.media.map((link, idx) => {
return <Image src={link} key={idx} alt={`Media for ${project.name}`} width={80} height={80} />
})}
</article>
)
}

View File

@@ -1,45 +0,0 @@
import { v4 } from "uuid"
import { cabin } from "@/app/layout";
type ElementType<Key extends keyof JSX.IntrinsicElements> = React.FC<JSX.IntrinsicElements[Key]>
type FormattedTags = {
[Key in keyof JSX.IntrinsicElements]: ElementType<Key>
}
const H1TAG: ElementType<"h1"> = ({ children }) => { return (
<h1 key={v4()} className={`text-4xl text-red-500 ${cabin.className} tracking-wide`}>{children}</h1>
)}
const H2Tag: ElementType<"h2"> = ({ children }) => (
<h2 key={v4()}>{children}</h2>
)
const H3Tag: ElementType<"h3"> = ({ children }) => (
<h3 key={v4()}>{children}</h3>
)
const H4Tag: ElementType<"h4"> = ({ children }) => (
<h4 key={v4()}>{children}</h4>
)
const PTag: ElementType<"p"> = ({ children }) => (
<p key={v4()}>{children}</p>
)
const LiTag: ElementType<"li"> = ({ children }) => (
<li key={v4()}>{children}</li>
)
const BrTag: ElementType<"br"> = () => (
<br key={v4()} />
)
export default {
"h1": H1TAG,
"h2": H2Tag,
"h3": H3Tag,
"h4": H4Tag,
"p": PTag,
"li": LiTag,
"br": BrTag
} satisfies Partial<FormattedTags>

View File

@@ -11,11 +11,11 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.367.0", "@aws-sdk/client-s3": "^3.367.0",
"@sendgrid/mail": "^7.7.0", "@sendgrid/mail": "^7.7.0",
"@supabase/supabase-js": "^2.26.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"eslint": "^8.46.0", "eslint": "^8.46.0",
"eslint-config-next": "^13.4.12", "eslint-config-next": "^13.4.12",
"next": "^13.4.12", "next": "^13.4.12",
"pg": "^8.11.3",
"postcss": "8.4.24", "postcss": "8.4.24",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@@ -26,9 +26,11 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.2.5", "@types/node": "20.2.5",
"@types/pg": "^8.10.3",
"@types/react": "18.2.7", "@types/react": "18.2.7",
"@types/react-dom": "18.2.4", "@types/react-dom": "18.2.4",
"@types/uuid": "^9.0.1", "@types/uuid": "^9.0.1",
"@vercel/style-guide": "^5.0.1" "@vercel/style-guide": "^5.0.1",
"zod": "^3.22.4"
} }
} }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,21 +1,15 @@
'use server';
import supabaseClient from "../services/supabase";
export default class WorkActions { export default class WorkActions {
static api = supabaseClient(); // static async getWork() {
// const { data, error } = await this.api.from("work").select("*");
static async getWork() { // if (error) throw error;
const { data, error } = await this.api.from("work").select("*"); // return data;
// }
if (error) throw error; // static async getWorkById(id: string) {
return data; // const { data, error } = await this.api.from("work").select("*").eq("id", id);
}
static async getWorkById(id: string) { // if (error) throw error;
const { data, error } = await this.api.from("work").select("*").eq("id", id); // return data;
// }
if (error) throw error;
return data;
}
} }

View File

@@ -1,9 +1,48 @@
type Project = { import { z } from "zod";
name: string;
startDate: Date; export type Project = {
endDate: Date; name: string;
current: boolean; description: string;
description: string; startDate: Date;
tagIDs: string[]; endDate?: Date;
media?: string[]; // array of URLs 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<Project>) {
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;
} }

19
server/services/pg.ts Normal file
View File

@@ -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
});
}

View File

@@ -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);
}