diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5a0cd01..36c747d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,5 +1,10 @@ name: build check (mikayla dot dev) +env: + POSTGRES_URL: ${{ secrets.POSTGRES_URL }} + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + on: push: branches: diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index 7851d24..f5a82d0 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -1,5 +1,10 @@ name: build check (mikayla dot dev) +env: + POSTGRES_URL: ${{ secrets.POSTGRES_URL }} + POSTGRES_USER: ${{ secrets.POSTGRES_USER }} + POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} + on: push: branches: diff --git a/app/about/education/page.tsx b/app/about/education/page.tsx deleted file mode 100644 index 41552c2..0000000 --- a/app/about/education/page.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function EducationPage() { - return <>Education Page -} diff --git a/app/about/music/page.tsx b/app/about/music/page.tsx deleted file mode 100644 index 4a604af..0000000 --- a/app/about/music/page.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import Link from 'next/link'; - -export default function MusicPage() { - return ( -
- -

Projects

- - - -

Works

- - - -

Stream

- -
- ) -} diff --git a/app/about/music/projects/page.tsx b/app/about/music/projects/page.tsx deleted file mode 100644 index 465a104..0000000 --- a/app/about/music/projects/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export default function MusicProjectPage() { - return ( -
-

Music Project Page

- -

This is where I'll keep a running list of my projects

-
- ) -} diff --git a/app/about/music/stream/page.tsx b/app/about/music/stream/page.tsx deleted file mode 100644 index 90ee54a..0000000 --- a/app/about/music/stream/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function MusicStreamingPage() { - return ( -
-

Music Streaming Page

-
- ) -} diff --git a/app/about/music/works/[id]/page.tsx b/app/about/music/works/[id]/page.tsx deleted file mode 100644 index 7b8ed18..0000000 --- a/app/about/music/works/[id]/page.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export default function MusicalWorkPage({ params }: { params: { id: string }}) { - if (Number.isNaN(parseInt(params.id))) { - return
Fail
- } - - return ( -
-

Music Works Page

-

Work No. {params.id}

- -
-

This page is coming soon!

-
-
- ) -} diff --git a/app/about/music/works/page.tsx b/app/about/music/works/page.tsx deleted file mode 100644 index 71b5733..0000000 --- a/app/about/music/works/page.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import Link from 'next/link'; - -export default function MusicWorksPage() { - return ( -
-

Music Works Page

- - -

First

- - - -

Second

- - - -

Third

- -
- ) -} diff --git a/app/about/work/[employer]/page.tsx b/app/about/work/[employer]/page.tsx deleted file mode 100644 index 86b3b4a..0000000 --- a/app/about/work/[employer]/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; -import { usePathname } from "next/navigation"; - -export default function ExperiencePage() { - const path = usePathname(); - - return ( -
-
-

Work Page

-

Employer: {path.split('/').at(-1)}

- -
-

This section is coming soon!

-
-
- ) -} diff --git a/app/about/work/page.tsx b/app/about/work/page.tsx deleted file mode 100644 index 8a6df70..0000000 --- a/app/about/work/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -const WorkHistory = () => { - return ( -
-

Work History

-
- ); -} - -export default WorkHistory; diff --git a/app/contact/page.tsx b/app/contact/page.tsx index 8fa44b5..b77ff07 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -1,5 +1,4 @@ 'use client'; -import { contactFormSubmit, testMailerSDK } from "@/server/actions/mailer.actions" import { useMemo, useState } from "react"; export default function ContactPage() { @@ -16,7 +15,7 @@ export default function ContactPage() {

Thanks for your interest! I'm looking forward to hearing from you.

-
+
diff --git a/app/layout.tsx b/app/layout.tsx index e73aaf4..77e3b48 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,56 +1,14 @@ -'use client' - import './globals.css' import Head from 'next/head' import Navbar from '@/components/Navbar' -import SiteTree from '@/components/SiteTree' import { Inter, Besley, Cabin } from 'next/font/google' -import { usePathname } from 'next/navigation' -import { IconContext } from 'react-icons' -import { useEffect, useState } from 'react' export const inter = Inter({ subsets: ['latin'] }) export const besley = Besley({ subsets: ['latin'] }) export const cabin = Cabin({ subsets: ['latin'] }) export default function RootLayout({ children }: { children: React.ReactNode }) { - const pathname = usePathname(); - const [bg, setBg] = useState('bg-slate-400 dark:bg-slate-900'); - const [overlay, setOverlay] = useState(false); - - const [pageIsScrolled, setPageIsScrolled] = useState(false); - - // useEffect(() => { - // if (pathname === '/contact') setOverlay(true); - - // switch (pathname) { - // case '/contact': - // setBg('bg-purple-300 dark:bg-darkPlum'); - // break; - // case '/about': - // setBg("bg-blue-100 dark:bg-slate-900"); - // break; - // case "/links": - // setBg("bg-sky-100 dark:bg-slate-900"); - // break; - // case '/': - // default: - // setBg('bg-slate-400 dark:bg-slate-900'); - // break; - // } - // }, [pathname]) - - useEffect(() => { - document.addEventListener('scroll', () => { - if (window.scrollY > 0) { - setPageIsScrolled(true); - } else { - setPageIsScrolled(false); - } - }) - }, []) - return ( @@ -60,14 +18,11 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - - - -
- - + +
+ ) 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/listen/[collectionid]/page.tsx b/app/listen/[collectionid]/page.tsx index 26baf5d..acb982d 100644 --- a/app/listen/[collectionid]/page.tsx +++ b/app/listen/[collectionid]/page.tsx @@ -1,3 +1,5 @@ -export default function ListenByCollectionID() { - return
ListenByCollectionID
; +import InProgress from "@/components/InProgress"; + +export default async function ListenByCollectionID() { + return } diff --git a/app/listen/layout.tsx b/app/listen/layout.tsx new file mode 100644 index 0000000..c9c3e24 --- /dev/null +++ b/app/listen/layout.tsx @@ -0,0 +1,7 @@ +export default function ListenLayout({ children }: { children: React.ReactNode}) { + return ( +
+ { children } +
+ ) +} diff --git a/app/listen/page.tsx b/app/listen/page.tsx index b32d406..4bcefaf 100644 --- a/app/listen/page.tsx +++ b/app/listen/page.tsx @@ -1,3 +1,11 @@ -export default function ListenIndex() { - return
ListenIndex
; +import InProgress from "@/components/InProgress"; + +export default async function ListenIndex() { + return ( +
+

Listen

+ {/* @ts-ignore server component */} + +
+ ) } 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..7d44554 100644 --- a/app/projects/[id]/page.tsx +++ b/app/projects/[id]/page.tsx @@ -1,12 +1,34 @@ -import { usePathname } from 'next/navigation' +// 'use client'; +import ProjectRepository from "@/server/actions/project.actions"; +import Image from "next/image"; -export default function ProjectById() { - const pathname = usePathname(); +export default async function ProjectById(req: { params: any, searchParams: any }) { + const projects = new ProjectRepository(); + const project = await projects.getProjectById(req.params.id); + + if (!project) { + return ( +
+

Project not found!

+
+ ) + } return ( -
-

ProjectById Page

-

Project ID: {pathname}

-
+
+
+

{project.name}

+

Started: {project.created.toLocaleString()}

+

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

+
+ +
+

{project.description}

+
+ + { project.media && project.media.map((link, idx) => { + return {`Media + })} +
) } diff --git a/app/projects/layout.tsx b/app/projects/layout.tsx new file mode 100644 index 0000000..1e8f0fd --- /dev/null +++ b/app/projects/layout.tsx @@ -0,0 +1,7 @@ +export default function ProjectsLayout({ children }: { children: React.ReactNode }) { + return ( +
+ { children } +
+ ) +} diff --git a/app/projects/page.tsx b/app/projects/page.tsx index 637831b..b2ab2e6 100644 --- a/app/projects/page.tsx +++ b/app/projects/page.tsx @@ -1,9 +1,10 @@ -export default function ProjectsPage() { +import InProgress from "@/components/InProgress"; + +export default async function ProjectsPage() { return (

Learn more about my work

- -

Contents of this page coming soon!

+
) } diff --git a/app/read/layout.tsx b/app/read/layout.tsx new file mode 100644 index 0000000..afcd642 --- /dev/null +++ b/app/read/layout.tsx @@ -0,0 +1,7 @@ +export default function ReadSectionLayout({ children }: { children: React.ReactNode }) { + return ( +
+ { children } +
+ ) +} diff --git a/app/read/page.tsx b/app/read/page.tsx index 90ca152..ebda7a1 100644 --- a/app/read/page.tsx +++ b/app/read/page.tsx @@ -1,3 +1,5 @@ -export default function BlogIndex() { - return
BlogIndex
; +import InProgress from "@/components/InProgress"; + +export default async function BlogIndex() { + return ; } 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/InProgress.tsx b/components/InProgress.tsx new file mode 100644 index 0000000..8427995 --- /dev/null +++ b/components/InProgress.tsx @@ -0,0 +1,3 @@ +export default function InProgress() { + return
Under construction! Come back soon.
; +} diff --git a/components/Navbar/index.tsx b/components/Navbar/index.tsx index 1696ac3..2fed560 100644 --- a/components/Navbar/index.tsx +++ b/components/Navbar/index.tsx @@ -1,14 +1,26 @@ +'use client'; import Link from 'next/link' -import { InlineLogo, useColorShift } from '../logo' -import { useState } from 'react'; +import { InlineLogo, useColorShift } from './logo' +import { useEffect, useState } from 'react'; import { RxActivityLog } from "react-icons/rx"; import { NavbarButton } from '../ui/Button'; const SHIFT_INTERVAL = 3000; -export default function Navbar({ pageIsScrolled = false }) { +export default function Navbar() { const navbarColorShift = useColorShift(SHIFT_INTERVAL); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [pageIsScrolled, setPageIsScrolled] = useState(false); + + useEffect(() => { + document.addEventListener('scroll', () => { + if (window.scrollY > 0) { + setPageIsScrolled(true); + } else { + setPageIsScrolled(false); + } + }) + }, []) return ( <> @@ -16,12 +28,8 @@ export default function Navbar({ pageIsScrolled = false }) { id="navbar" className={` w-full z-50 fixed flex flex-nowrap items-baseline justify-apart px-8 py-4 - ${mobileMenuOpen - ? "bg-slate-300 dark:bg-[#131313] " - : pageIsScrolled - ? "bg-slate-300 dark:bg-black " - : "bg-inherit " - }text-white transition-all duration-200` + ${pageIsScrolled ? "bg-slate-300 dark:bg-black " : "bg-inherit "} + text-white transition-all duration-200` }> 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/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/env.mjs b/env.mjs new file mode 100644 index 0000000..e2ab82f --- /dev/null +++ b/env.mjs @@ -0,0 +1,17 @@ +import { createEnv } from "@t3-oss/env-nextjs"; +import { z } from 'zod'; + +const env = createEnv({ + server: { + POSTGRES_URL: z.string().url(), + POSTGRES_USER: z.string(), + POSTGRES_PASSWORD: z.string(), + }, + runtimeEnv: { + POSTGRES_URL: process.env.POSTGRES_URL, + POSTGRES_USER: process.env.POSTGRES_USER, + POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD, + } +}) + +export { env } 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/next.config.js b/next.config.js index 5ccd269..fae793b 100644 --- a/next.config.js +++ b/next.config.js @@ -2,10 +2,6 @@ const nextConfig = { pageExtensions: ['js', 'jsx', 'ts', 'tsx'], reactStrictMode: true, - experimental: { - // mdxRs: true, - serverActions: true, - } } module.exports = nextConfig; diff --git a/package.json b/package.json index b0f1db6..08dc84e 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,13 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.367.0", - "@sendgrid/mail": "^7.7.0", - "@supabase/supabase-js": "^2.26.0", + "@t3-oss/env-nextjs": "^0.7.0", "autoprefixer": "10.4.14", "eslint": "^8.46.0", "eslint-config-next": "^13.4.12", "next": "^13.4.12", - "postcss": "8.4.24", + "pg": "^8.11.3", + "postcss": "^8.4.31", "react": "18.2.0", "react-dom": "18.2.0", "react-icons": "^4.9.0", @@ -26,9 +26,12 @@ }, "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", + "ts-node": "^10.9.1", + "zod": "^3.22.4" } } diff --git a/pkg/.eslintrc.json b/pkg/.eslintrc.json new file mode 100644 index 0000000..6751428 --- /dev/null +++ b/pkg/.eslintrc.json @@ -0,0 +1,20 @@ +{ + "env": { + "browser": true, + "es2021": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + } +} diff --git a/pkg/.gitignore b/pkg/.gitignore new file mode 100644 index 0000000..a65b417 --- /dev/null +++ b/pkg/.gitignore @@ -0,0 +1 @@ +lib diff --git a/pkg/.npmignore b/pkg/.npmignore new file mode 100644 index 0000000..f950ebe --- /dev/null +++ b/pkg/.npmignore @@ -0,0 +1,2 @@ +src +eslintrc.json diff --git a/pkg/README.md b/pkg/README.md new file mode 100644 index 0000000..2ebf4fc --- /dev/null +++ b/pkg/README.md @@ -0,0 +1,2 @@ +# Helpers for mikayla.dev + diff --git a/pkg/package.json b/pkg/package.json new file mode 100644 index 0000000..f4af124 --- /dev/null +++ b/pkg/package.json @@ -0,0 +1,29 @@ +{ + "name": "@mkladotdev/utils", + "version": "0.0.1", + "description": "Utilities for mikayla.dev", + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "files": [ + "./lib/**/*" + ], + "scripts": { + "build": "tsc", + "lint": "eslint . --ext .ts", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "Mikayla Dobson", + "license": "ISC", + "dependencies": {}, + "devDependencies": { + "@types/node": "^20.5.9", + "@typescript-eslint/eslint-plugin": "^6.6.0", + "@typescript-eslint/parser": "^6.6.0", + "eslint": "^8.49.0", + "typescript": "^5.2.2" + }, + "publishConfig": { + "registry": "http://localhost:4873" + } +} diff --git a/pkg/src/entities/audiocollection.ts b/pkg/src/entities/audiocollection.ts new file mode 100644 index 0000000..28eccb1 --- /dev/null +++ b/pkg/src/entities/audiocollection.ts @@ -0,0 +1,12 @@ +export type Track = { + name: string; + date: Date; + description: string; +} + +export type AudioCollection = { + name: string; + date: Date; + tracklist: Track[]; + directory: string; +} diff --git a/pkg/src/entities/post.ts b/pkg/src/entities/post.ts new file mode 100644 index 0000000..f2f7fb8 --- /dev/null +++ b/pkg/src/entities/post.ts @@ -0,0 +1,9 @@ +export type Post = { + name: string; + date: Date; + author: string; + description: string; + body: string; + tagIDs: string[]; + media?: string[]; // array of URLs +} diff --git a/pkg/src/entities/project.ts b/pkg/src/entities/project.ts new file mode 100644 index 0000000..a414e63 --- /dev/null +++ b/pkg/src/entities/project.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; + +export type Project = { + name: string; + description: string; + created: Date; + updated?: 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(), + created: z.date(), + updated: 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, + created: data.created || today, + updated: today, + } satisfies Partial; + + 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/pkg/tsconfig.json b/pkg/tsconfig.json new file mode 100644 index 0000000..3b06fcf --- /dev/null +++ b/pkg/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + /* Projects */ + "incremental": true, + + /* Language and Environment */ + "target": "es2016", + + /* Modules */ + "module": "commonjs", + "rootDir": "./src", + + /* Emit */ + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./lib", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + + /* Type Checking */ + "strict": true, + "noImplicitAny": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["lib", "node_modules"] +} diff --git a/public/resume/Mikayla Resume 0623.docx b/public/resume/Mikayla Resume 1023.docx similarity index 97% rename from public/resume/Mikayla Resume 0623.docx rename to public/resume/Mikayla Resume 1023.docx index 7f55bd7..65922bd 100644 Binary files a/public/resume/Mikayla Resume 0623.docx and b/public/resume/Mikayla Resume 1023.docx differ diff --git a/public/resume/Mikayla Resume 0623.pdf b/public/resume/Mikayla Resume 1023.pdf similarity index 58% rename from public/resume/Mikayla Resume 0623.pdf rename to public/resume/Mikayla Resume 1023.pdf index 396fad4..b53de23 100644 Binary files a/public/resume/Mikayla Resume 0623.pdf and b/public/resume/Mikayla Resume 1023.pdf differ diff --git a/server/actions/mailer.actions.ts b/server/actions/mailer.actions.ts deleted file mode 100644 index e728c47..0000000 --- a/server/actions/mailer.actions.ts +++ /dev/null @@ -1,34 +0,0 @@ -'use server'; -import { Mailer } from "../services/sendgrid"; - -export async function contactFormSubmit(e: FormData) { - const data = [e.get('email'), e.get('message'), e.get('name')].map(each => each?.valueOf()); - const mailer = new Mailer(); - - data.forEach(item => { - if (typeof item !== 'string') throw new Error('Invalid form data') - }) - - const result = await mailer.send(...data as [string, string, string]); - return result; -} - -export async function testMailerSDK(e: FormData) { - const sgMail = require('@sendgrid/mail') - sgMail.setApiKey(process.env.SENDGRID_API_KEY) - const msg = { - to: 'mikaylaherself@gmail.com', // Change to your recipient - from: 'me@mikayla.dev', // Change to your verified sender - subject: 'Sending with SendGrid is Fun', - text: 'and easy to do anywhere, even with Node.js', - html: 'and easy to do anywhere, even with Node.js', - } - sgMail - .send(msg) - .then(() => { - console.log('Email sent') - }) - .catch((error: unknown) => { - console.error(error) - }) -} diff --git a/server/actions/project.actions.ts b/server/actions/project.actions.ts new file mode 100644 index 0000000..8587e7c --- /dev/null +++ b/server/actions/project.actions.ts @@ -0,0 +1,56 @@ +import { Project, isProject } from "../entities/project"; +import createClient from "../services/pg"; + +export default class ProjectRepository { + async createProject(data: Project) { + const client = await createClient(); + if (!client) return null; + await client.connect(); + + const { rows } = await client.query( + "INSERT INTO project (name, description, created, updated) VALUES ($1, $2, $3, $4) RETURNING *" + , [ + data.name, + data.description, + data.created, + data.updated, + ]); + + await client.end(); + + if (rows.every(row => isProject(row))) { + return rows[0] as Project; + } + + return null; + } + + async getProjects() { + const client = await createClient(); + if (!client) return null; + + const { rows } = await client.query("SELECT * FROM project"); + await client.end(); + + if (rows.every(row => isProject(row))) { + return rows as Project[]; + } + + return null; + } + + async getProjectById(id: string) { + const client = await createClient(); + if (!client) return null; + await client.connect(); + + const { rows } = await client.query("SELECT * FROM project 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 deleted file mode 100644 index 86d153b..0000000 --- a/server/actions/work.actions.ts +++ /dev/null @@ -1,21 +0,0 @@ -'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("*"); - - if (error) throw error; - return data; - } - - static async getWorkById(id: string) { - const { data, error } = await this.api.from("work").select("*").eq("id", id); - - if (error) throw error; - return data; - } -} diff --git a/server/entities/project.ts b/server/entities/project.ts index 270f5b6..a414e63 100644 --- a/server/entities/project.ts +++ b/server/entities/project.ts @@ -1,9 +1,49 @@ -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; + created: Date; + updated?: 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(), + created: z.date(), + updated: 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, + created: data.created || today, + updated: today, + } satisfies Partial; + + 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..dd2d9fb --- /dev/null +++ b/server/services/pg.ts @@ -0,0 +1,24 @@ +import { env } from "@/env.mjs"; +import pg from 'pg'; +const { Client } = pg; + +export class PostgresError extends Error { + constructor(message: string) { + super(message); + this.name = "PostgresError"; + } +} + +export default async function createClient() { + try { + return new Client({ + connectionString: env.POSTGRES_URL, + user: env.POSTGRES_USER, + password: env.POSTGRES_PASSWORD, + ssl: { rejectUnauthorized: false } + }); + } catch(e) { + console.log('error creating client', e); + return null; + } +} diff --git a/server/services/sendgrid.ts b/server/services/sendgrid.ts deleted file mode 100644 index 5df1818..0000000 --- a/server/services/sendgrid.ts +++ /dev/null @@ -1,28 +0,0 @@ -import mailer, { ClientResponse, MailDataRequired, MailService } from "@sendgrid/mail"; - -export class Mailer { - private mailer: MailService; - - constructor() { - const service = mailer; - const key = process.env.SENDGRID_API_KEY; - - if (!key) throw new Error("No SendGrid API key provided"); - - service.setApiKey(key); - - this.mailer = service; - } - - public async send(from: string, text: string, name: string) { - const data: MailDataRequired = { - text, from, - cc: from, - to: 'hello@mikayla.dev', - subject: `Contact form submission from ${name}` - } - const result = await this.mailer.send(data); - return result[0] as ClientResponse; - } -} - 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); -}