5
.github/workflows/main.yml
vendored
5
.github/workflows/main.yml
vendored
@@ -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:
|
||||
|
||||
5
.github/workflows/staging.yml
vendored
5
.github/workflows/staging.yml
vendored
@@ -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:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function EducationPage() {
|
||||
return <>Education Page</>
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function MusicPage() {
|
||||
return (
|
||||
<div>
|
||||
<Link href="/about/music/projects">
|
||||
<p>Projects</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/about/music/works">
|
||||
<p>Works</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/about/music/stream">
|
||||
<p>Stream</p>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export default function MusicProjectPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Music Project Page</h1>
|
||||
|
||||
<p>This is where I'll keep a running list of my projects</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
export default function MusicStreamingPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Music Streaming Page</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
export default function MusicalWorkPage({ params }: { params: { id: string }}) {
|
||||
if (Number.isNaN(parseInt(params.id))) {
|
||||
return <div>Fail</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Music Works Page</h1>
|
||||
<p>Work No. {params.id}</p>
|
||||
|
||||
<div>
|
||||
<p>This page is coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function MusicWorksPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Music Works Page</h1>
|
||||
|
||||
<Link href="/about/music/works/1">
|
||||
<p>First</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/about/music/works/2">
|
||||
<p>Second</p>
|
||||
</Link>
|
||||
|
||||
<Link href="/about/music/works/3">
|
||||
<p>Third</p>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
'use client';
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export default function ExperiencePage() {
|
||||
const path = usePathname();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="spacer" className='h-[6rem] w-full' />
|
||||
<h1>Work Page</h1>
|
||||
<p>Employer: {path.split('/').at(-1)}</p>
|
||||
|
||||
<div>
|
||||
<p>This section is coming soon!</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const WorkHistory = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Work History</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WorkHistory;
|
||||
@@ -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() {
|
||||
<div className="flex flex-col mx-24 items-center dark:text-white ">
|
||||
<h1 className="text-3xl my-8 place-self-start">Thanks for your interest! I'm looking forward to hearing from you.</h1>
|
||||
|
||||
<form action={testMailerSDK} className="w-full">
|
||||
<form className="w-full">
|
||||
<div className="flex w-full">
|
||||
<div className="flex flex-col w-1/2 mr-2">
|
||||
<label htmlFor="name">Name</label>
|
||||
|
||||
@@ -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 (
|
||||
<html lang="en">
|
||||
<Head>
|
||||
@@ -60,14 +18,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<body className={inter.className}>
|
||||
<Navbar pageIsScrolled={pageIsScrolled} />
|
||||
<SiteTree />
|
||||
<IconContext.Provider value={{}}>
|
||||
<div>
|
||||
<div id="navbar-spacer" className="h-[6rem] w-full bg-slate-300 dark:bg-black " />
|
||||
{children}
|
||||
</div>
|
||||
</IconContext.Provider>
|
||||
<Navbar />
|
||||
<div>
|
||||
<div id="navbar-spacer" className="h-[6rem] w-full bg-slate-300 dark:bg-black " />
|
||||
{children}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export default function ListenByCollectionID() {
|
||||
return <div>ListenByCollectionID</div>;
|
||||
import InProgress from "@/components/InProgress";
|
||||
|
||||
export default async function ListenByCollectionID() {
|
||||
return <InProgress />
|
||||
}
|
||||
|
||||
7
app/listen/layout.tsx
Normal file
7
app/listen/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function ListenLayout({ children }: { children: React.ReactNode}) {
|
||||
return (
|
||||
<main className="min-h-screen bg-gradient-to-b from-slate-300 to-cyan-800 dark:from-black dark:to-cyan-900 text-black dark:text-white">
|
||||
{ children }
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
export default function ListenIndex() {
|
||||
return <div>ListenIndex</div>;
|
||||
import InProgress from "@/components/InProgress";
|
||||
|
||||
export default async function ListenIndex() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Listen</h1>
|
||||
{/* @ts-ignore server component */}
|
||||
<InProgress />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,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 (
|
||||
<div>
|
||||
<h1>Project not found!</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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.created.toLocaleString()}</p>
|
||||
<p>{project.updated ? `Finished: ${project.updated.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>
|
||||
)
|
||||
}
|
||||
|
||||
7
app/projects/layout.tsx
Normal file
7
app/projects/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function ProjectsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-300 to-sky-700 dark:from-black dark:to-green-950 text-black dark:text-white">
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
export default function ProjectsPage() {
|
||||
import InProgress from "@/components/InProgress";
|
||||
|
||||
export default async function ProjectsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Learn more about my work</h1>
|
||||
|
||||
<p>Contents of this page coming soon!</p>
|
||||
<InProgress />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
7
app/read/layout.tsx
Normal file
7
app/read/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function ReadSectionLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-b from-slate-300 to-fuchsia-100 dark:from-black dark:to-fuchsia-900 text-black dark:text-white">
|
||||
{ children }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export default function BlogIndex() {
|
||||
return <div>BlogIndex</div>;
|
||||
import InProgress from "@/components/InProgress";
|
||||
|
||||
export default async function BlogIndex() {
|
||||
return <InProgress />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useColorShift } from "../logo";
|
||||
import { useColorShift } from "../Navbar/logo";
|
||||
|
||||
export const ColorChangeName = () => {
|
||||
const { firstColor, secondColor, thirdColor } = useColorShift(14000);
|
||||
|
||||
3
components/InProgress.tsx
Normal file
3
components/InProgress.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function InProgress() {
|
||||
return <div>Under construction! Come back soon.</div>;
|
||||
}
|
||||
@@ -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`
|
||||
}>
|
||||
<Link passHref href="/" className="w-1/4">
|
||||
<InlineLogo customHookInstance={navbarColorShift} />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
17
env.mjs
Normal file
17
env.mjs
Normal file
@@ -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 }
|
||||
@@ -2,10 +2,6 @@
|
||||
const nextConfig = {
|
||||
pageExtensions: ['js', 'jsx', 'ts', 'tsx'],
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
// mdxRs: true,
|
||||
serverActions: true,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
11
package.json
11
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"
|
||||
}
|
||||
}
|
||||
|
||||
20
pkg/.eslintrc.json
Normal file
20
pkg/.eslintrc.json
Normal file
@@ -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": {
|
||||
}
|
||||
}
|
||||
1
pkg/.gitignore
vendored
Normal file
1
pkg/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
lib
|
||||
2
pkg/.npmignore
Normal file
2
pkg/.npmignore
Normal file
@@ -0,0 +1,2 @@
|
||||
src
|
||||
eslintrc.json
|
||||
2
pkg/README.md
Normal file
2
pkg/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# Helpers for mikayla.dev
|
||||
|
||||
29
pkg/package.json
Normal file
29
pkg/package.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
12
pkg/src/entities/audiocollection.ts
Normal file
12
pkg/src/entities/audiocollection.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type Track = {
|
||||
name: string;
|
||||
date: Date;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export type AudioCollection = {
|
||||
name: string;
|
||||
date: Date;
|
||||
tracklist: Track[];
|
||||
directory: string;
|
||||
}
|
||||
9
pkg/src/entities/post.ts
Normal file
9
pkg/src/entities/post.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type Post = {
|
||||
name: string;
|
||||
date: Date;
|
||||
author: string;
|
||||
description: string;
|
||||
body: string;
|
||||
tagIDs: string[];
|
||||
media?: string[]; // array of URLs
|
||||
}
|
||||
49
pkg/src/entities/project.ts
Normal file
49
pkg/src/entities/project.ts
Normal file
@@ -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<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,
|
||||
created: data.created || today,
|
||||
updated: today,
|
||||
} satisfies Partial<Project>;
|
||||
|
||||
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;
|
||||
}
|
||||
28
pkg/tsconfig.json
Normal file
28
pkg/tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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: '<strong>and easy to do anywhere, even with Node.js</strong>',
|
||||
}
|
||||
sgMail
|
||||
.send(msg)
|
||||
.then(() => {
|
||||
console.log('Email sent')
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
56
server/actions/project.actions.ts
Normal file
56
server/actions/project.actions.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 +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;
|
||||
}
|
||||
}
|
||||
@@ -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<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,
|
||||
created: data.created || today,
|
||||
updated: today,
|
||||
} satisfies Partial<Project>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
24
server/services/pg.ts
Normal file
24
server/services/pg.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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