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