1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@
|
||||
/.pnp
|
||||
.pnp.js
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client';
|
||||
import { submitMessage } from "@/server/actions/contact.actions";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export default function ContactPage() {
|
||||
@@ -15,7 +16,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 className="w-full">
|
||||
<form className="w-full" action={async () => await submitMessage({ from: email, text: message })}>
|
||||
<div className="flex w-full">
|
||||
<div className="flex flex-col w-1/2 mr-2">
|
||||
<label htmlFor="name">Name</label>
|
||||
|
||||
@@ -4,7 +4,6 @@ export default async function ListenIndex() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Listen</h1>
|
||||
{/* @ts-ignore server component */}
|
||||
<InProgress />
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
// 'use client';
|
||||
import ProjectRepository from "@/server/actions/project.actions";
|
||||
import Image from "next/image";
|
||||
|
||||
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 (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -37,9 +37,6 @@ export default function Navbar() {
|
||||
|
||||
<div className="hidden md:inline-flex justify-end w-3/4">
|
||||
<NavbarButton href="/about" label="About" />
|
||||
<NavbarButton href="/projects" label="Projects" />
|
||||
<NavbarButton href="/read" label="Read" />
|
||||
<NavbarButton href="/listen" label="Listen" />
|
||||
<NavbarButton href="/links" label="Links" />
|
||||
<NavbarButton href="/contact" label="Contact" />
|
||||
</div>
|
||||
@@ -60,8 +57,8 @@ export default function Navbar() {
|
||||
<p className='text-lg text-right text-white text-opacity-80 hover:text-opacity-100 uppercase p-2 border-opacity-50 hover:border-opacity-75'>About</p>
|
||||
</Link>
|
||||
|
||||
<Link onClick={() => setMobileMenuOpen(false)} passHref href="/projects" className="w-auto px-2">
|
||||
<p className='text-lg text-right text-white text-opacity-80 hover:text-opacity-100 hover:border-opacity-75 uppercase p-2 border-opacity-50'>Projects</p>
|
||||
<Link onClick={() => setMobileMenuOpen(false)} passHref href="/links" className="w-auto px-2">
|
||||
<p className='text-lg text-right text-white text-opacity-80 hover:text-opacity-100 hover:border-opacity-75 uppercase p-2 border-opacity-50'>Links</p>
|
||||
</Link>
|
||||
|
||||
<Link onClick={() => setMobileMenuOpen(false)} passHref href="/contact" className="w-auto px-2">
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
import Link from "next/link";
|
||||
import Card from "../ui/Card";
|
||||
|
||||
const Experience = () => {
|
||||
export default function Experience() {
|
||||
return (
|
||||
<section className="w-full">
|
||||
<Card>
|
||||
<Link target="_blank" referrerPolicy="no-referrer" href="https://vertafore.com" className="uppercase text-2xl text-rose-600 hover:text-rose-400 active:text-rose-600">Vertafore</Link>
|
||||
<p className="font-light italic text-black dark:text-rose-300">Software Engineer</p>
|
||||
<p className="dark:text-white">May 2024 - present</p>
|
||||
|
||||
<div className="h-[1px] w-full my-3 bg-rose-600 dark:bg-rose-300" />
|
||||
|
||||
<p className="dark:text-white leading-relaxed font-light">Contributing as part of a development team building an insurance tech solution.</p>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Link target="_blank" referrerPolicy="no-referrer" href="https://epicstockmedia.com" className="uppercase text-2xl text-rose-600 hover:text-rose-400 active:text-rose-600">Epic Stock Media</Link>
|
||||
<p className="font-light italic text-black dark:text-rose-300">Software Engineer</p>
|
||||
<p className="dark:text-white">Dec 2023 - May 2024</p>
|
||||
|
||||
<div className="h-[1px] w-full my-3 bg-rose-600 dark:bg-rose-300" />
|
||||
|
||||
<p className="dark:text-white leading-relaxed font-light">Building a dedicated tool for sound design professionals to browse, stream, and download from a large library of audio assets in an on-premise managed cloud solution.</p>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Link target="_blank" referrerPolicy="no-referrer" href="https://dropper.studio" className="uppercase text-2xl text-rose-600 hover:text-rose-400 active:text-rose-600">Dropper Studio</Link>
|
||||
<p className="font-light italic text-black dark:text-rose-300">Nashville, TN (hybrid) - Software Engineer</p>
|
||||
<p className="font-light italic text-black dark:text-rose-300">Software Engineer</p>
|
||||
<p className="dark:text-white">March 2023 - present</p>
|
||||
|
||||
<div className="h-[1px] w-full my-3 bg-rose-600 dark:bg-rose-300" />
|
||||
|
||||
<p className="dark:text-white leading-relaxed font-light">Building a full-stack e-commerce platform for the music industry. Experience includes: producing a functional proof of concept from design specifications; constructing a scalable, performant full-stack architecture; and project/team management skills.</p>
|
||||
|
||||
<Link href="/about/work/dropper" className="text-rose-600 hover:text-rose-400 active:text-rose-300">Learn more about my work with Dropper</Link>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Link target="_blank" referrerPolicy="no-referrer" href="https://dization.com/" className="uppercase text-2xl text-rose-600 hover:text-rose-400 active:text-rose-600">Dization, Inc.</Link>
|
||||
<p className="font-light italic text-black dark:text-rose-300">Pittsburgh, PA (remote) - Software Engineer (intern)</p>
|
||||
<p className="font-light italic text-black dark:text-rose-300">Software Engineer (intern)</p>
|
||||
<p className="dark:text-white">October 2022 - March 2023</p>
|
||||
|
||||
<div className="h-[1px] w-full my-3 bg-rose-600 dark:bg-rose-300" />
|
||||
@@ -30,17 +48,13 @@ const Experience = () => {
|
||||
|
||||
<Card>
|
||||
<h3 className="uppercase text-2xl text-rose-600">Metazu Studio</h3>
|
||||
<p className="font-light italic text-black dark:text-rose-300">Nashville, TN (hybrid) - Software Engineer (consultant)</p>
|
||||
<p className="font-light italic text-black dark:text-rose-300">Software Engineer (consultant)</p>
|
||||
<p className="dark:text-white">March 2022 - December 2022</p>
|
||||
|
||||
<div className="h-[1px] w-full my-3 bg-rose-600 dark:bg-rose-300" />
|
||||
|
||||
<p className="dark:text-white leading-relaxed font-light">Consulted on small teams for the design and engineering of full-stack web applications for clients. Used technologies including Node.js, React, MongoDB, and PostgreSQL.</p>
|
||||
</Card>
|
||||
|
||||
<Link href="/about/work" className="text-rose-300 hover:text-rose-500 active:text-rose-300 bg-slate-950 p-2 rounded-lg shadow-lg">See more about my experience</Link>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default Experience;
|
||||
|
||||
@@ -13,9 +13,9 @@ const Projects = () => (
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<h3 className="uppercase text-2xl text-rose-600">Recipin</h3>
|
||||
<h3 className="uppercase text-2xl text-rose-600">Unbinder</h3>
|
||||
<p className="font-light italic text-rose-300">October 2022 - present</p>
|
||||
<p className="text-rose-300">React, Express, TypeScript, PostgreSQL</p>
|
||||
<p className="text-rose-300">ASP.NET</p>
|
||||
|
||||
<div className="h-[1px] w-full my-3 bg-rose-600 dark:bg-rose-300" />
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ const Skills = () => (
|
||||
<div className="opacity-0 group-open:opacity-100 transition-opacity duration-500 flex flex-wrap">
|
||||
<Chip label="Typescript" href="https://github.com/innocuous-symmetry?tab=repositories&language=typescript" />
|
||||
<Chip label="React" href="https://github.com/innocuous-symmetry?tab=repositories&q=react" />
|
||||
<Chip label="AWS S3 SDK" />
|
||||
<Chip label="Next.js" />
|
||||
<Chip label="AWS S3 SDK" />
|
||||
<Chip label="tRPC" />
|
||||
<Chip label="React Query" />
|
||||
<Chip label="jQuery" />
|
||||
@@ -53,7 +53,6 @@ const Skills = () => (
|
||||
<div className="opacity-0 group-open:opacity-100 transition-opacity duration-500 flex flex-wrap">
|
||||
<Chip label="Micropython" href="https://github.com/innocuous-symmetry/picosynth" />
|
||||
<Chip label="Flask" />
|
||||
<Chip label="Flet" />
|
||||
</div>
|
||||
</details>
|
||||
</article>
|
||||
@@ -80,6 +79,7 @@ const Skills = () => (
|
||||
<RxChevronDown className="transition group-open:rotate-180" />
|
||||
</summary>
|
||||
<div className="opacity-0 group-open:opacity-100 transition-opacity duration-500 flex flex-wrap">
|
||||
<Chip label="Virtualization / Proxmox" />
|
||||
<Chip label="Docker" />
|
||||
<Chip label="Github Actions" />
|
||||
<Chip label="Git / Github" />
|
||||
@@ -125,24 +125,11 @@ const Skills = () => (
|
||||
<RxChevronDown className="transition group-open:rotate-180" />
|
||||
</summary>
|
||||
<div className="opacity-0 group-open:opacity-100 transition-opacity duration-500 flex flex-wrap">
|
||||
<Chip label="Ruby / Rails" />
|
||||
<Chip label="C# / ASP.NET" />
|
||||
<Chip label="Golang" />
|
||||
</div>
|
||||
</details>
|
||||
</article>
|
||||
|
||||
{/* <article className="mt-2">
|
||||
<details className="group">
|
||||
<summary className="flex items-center text-rose-600 dark:text-rose-300 uppercase tracking-wide text-lg mb-2 list-none">
|
||||
<p className="mr-2">Natural Language Processing</p>
|
||||
<RxChevronDown className="transition group-open:rotate-180" />
|
||||
</summary>
|
||||
<div className="opacity-0 group-open:opacity-100 transition-opacity duration-500 flex flex-wrap">
|
||||
<p className="py-0.5 px-2 bg-rose-900 rounded-xl m-0.5">NLPT</p>
|
||||
<p className="py-0.5 px-2 bg-rose-900 rounded-xl m-0.5">Spacy</p>
|
||||
</div>
|
||||
</details>
|
||||
</article> */}
|
||||
</div>
|
||||
|
||||
<details className="group bg-slate-400 dark:bg-slate-800 dark:bg-opacity-40 p-3 my-4 rounded-lg">
|
||||
|
||||
16
env.mjs
16
env.mjs
@@ -3,15 +3,17 @@ import { z } from 'zod';
|
||||
|
||||
const env = createEnv({
|
||||
server: {
|
||||
POSTGRES_URL: z.string().url(),
|
||||
POSTGRES_USER: z.string(),
|
||||
POSTGRES_PASSWORD: z.string(),
|
||||
SMTP_USER: z.string().optional(),
|
||||
SMTP_PASS: z.string().optional(),
|
||||
SMTP_TO: z.string().optional(),
|
||||
SMTP_HOST: z.string().optional(),
|
||||
},
|
||||
runtimeEnv: {
|
||||
POSTGRES_URL: process.env.POSTGRES_URL,
|
||||
POSTGRES_USER: process.env.POSTGRES_USER,
|
||||
POSTGRES_PASSWORD: process.env.POSTGRES_PASSWORD,
|
||||
}
|
||||
SMTP_USER: process.env.SMTP_USER,
|
||||
SMTP_PASS: process.env.SMTP_PASS,
|
||||
SMTP_TO: process.env.SMTP_TO,
|
||||
SMTP_HOST: process.env.SMTP_HOST,
|
||||
},
|
||||
})
|
||||
|
||||
export { env }
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
const nextConfig = {
|
||||
pageExtensions: ['js', 'jsx', 'ts', 'tsx'],
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
serverActions: true,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-next": "^13.4.12",
|
||||
"next": "^13.4.12",
|
||||
"pg": "^8.11.3",
|
||||
"nodemailer": "^6.9.13",
|
||||
"postcss": "^8.4.31",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
@@ -26,6 +26,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.2.5",
|
||||
"@types/nodemailer": "^6.4.15",
|
||||
"@types/pg": "^8.10.3",
|
||||
"@types/react": "18.2.7",
|
||||
"@types/react-dom": "18.2.4",
|
||||
|
||||
32
server/actions/contact.actions.ts
Normal file
32
server/actions/contact.actions.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
'use server';
|
||||
import { createTransport } from "nodemailer";
|
||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||
import { env } from "../../env.mjs"
|
||||
|
||||
export async function submitMessage({ from, text }: { from: string, text: string }) {
|
||||
const options = {
|
||||
host: env.SMTP_HOST,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: env.SMTP_USER,
|
||||
pass: env.SMTP_PASS,
|
||||
},
|
||||
} as SMTPTransport.Options;
|
||||
|
||||
// console.log({ options });
|
||||
|
||||
// const transport = createTransport(options);
|
||||
|
||||
// const result = await transport.sendMail({
|
||||
// subject: "Contact Form Submission | mikayla.dev",
|
||||
// to: env.SMTP_TO,
|
||||
// from,
|
||||
// text,
|
||||
// });
|
||||
|
||||
// const failed = result.rejected.concat(result.pending).filter(Boolean);
|
||||
|
||||
// if (failed.length) {
|
||||
// throw new Error("Failed to send email verification");
|
||||
// }
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
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,24 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user