in progress: build out server side
This commit is contained in:
@@ -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() {
|
||||
|
||||
@@ -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 (
|
||||
<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">
|
||||
|
||||
@@ -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 (
|
||||
<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">
|
||||
|
||||
@@ -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 <p>Project not found!</p>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>ProjectById Page</h1>
|
||||
<p>Project ID: {pathname}</p>
|
||||
</div>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default function ProjectsPage() {
|
||||
export default async function ProjectsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Learn more about my work</h1>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useColorShift } from "../logo";
|
||||
import { useColorShift } from "../Navbar/logo";
|
||||
|
||||
export const ColorChangeName = () => {
|
||||
const { firstColor, secondColor, thirdColor } = useColorShift(14000);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
29
components/Projects/ProjectEntry.tsx
Normal file
29
components/Projects/ProjectEntry.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
34
server/actions/project.actions.ts
Normal file
34
server/actions/project.actions.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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<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
19
server/services/pg.ts
Normal 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
|
||||
});
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user