improved setup for server components

This commit is contained in:
2023-10-08 12:39:58 -05:00
parent a3ff7598ae
commit c5691ae9d2
12 changed files with 85 additions and 155 deletions

View File

@@ -1,5 +1,4 @@
'use client'; 'use client';
import { testMailerSDK } from "@/server/actions/mailer.actions"
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
export default function ContactPage() { export default function ContactPage() {
@@ -16,7 +15,7 @@ export default function ContactPage() {
<div className="flex flex-col mx-24 items-center dark:text-white "> <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&apos;m looking forward to hearing from you.</h1> <h1 className="text-3xl my-8 place-self-start">Thanks for your interest! I&apos;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 w-full">
<div className="flex flex-col w-1/2 mr-2"> <div className="flex flex-col w-1/2 mr-2">
<label htmlFor="name">Name</label> <label htmlFor="name">Name</label>

View File

@@ -1,30 +1,25 @@
import Image from "next/image"; // 'use client';
import ProjectRepository from "@/server/actions/project.actions"; import ProjectRepository from "@/server/actions/project.actions";
import Image from "next/image";
interface PageProps { export default async function ProjectById(req: { params: any, searchParams: any }) {
searchParams: any;
params: {
id: any;
}
children: React.ReactNode;
}
export default async function ProjectById(props: PageProps) {
const { id } = props.params;
const projects = new ProjectRepository(); const projects = new ProjectRepository();
const project = await projects.getProjectById(id); const project = await projects.getProjectById(req.params.id);
if (!project) { if (!project) {
return <p>Project not found!</p> return (
<div>
<h1>Project not found!</h1>
</div>
)
} }
return ( return (
<article id="project-entry-body"> <article id="project-entry-body">
<header> <header>
<h1 className="text-4xl font-bold">{project.name}</h1> <h1 className="text-4xl font-bold">{project.name}</h1>
{/* <p>Started: {project.startDate.toLocaleString()}</p> <p>Started: {project.created.toLocaleString()}</p>
<p>{project.endDate ? `Finished: ${project.endDate.toLocaleDateString()}` : "(In progress)"}</p> */} <p>{project.updated ? `Finished: ${project.updated.toLocaleDateString()}` : "(In progress)"}</p>
</header> </header>
<div id="project-entry-content" className="flex flex-col"> <div id="project-entry-content" className="flex flex-col">

View File

@@ -1,29 +0,0 @@
import ProjectRepository from "@/server/actions/project.actions"
import Image from "next/image"
export default async function ProjectEntry({ id }: { id: string }) {
const projects = new ProjectRepository();
const project = await projects.getProjectById(id);
if (!project) {
return <p>Project not found!</p>
}
return (
<article id="project-entry-body">
<header>
<h1 className="text-4xl font-bold">{project.name}</h1>
<p>Started: {project.startDate.toLocaleString()}</p>
<p>{project.endDate ? `Finished: ${project.endDate.toLocaleDateString()}` : "(In progress)"}</p>
</header>
<div id="project-entry-content" className="flex flex-col">
<p>{project.description}</p>
</div>
{ project.media && project.media.map((link, idx) => {
return <Image src={link} key={idx} alt={`Media for ${project.name}`} width={80} height={80} />
})}
</article>
)
}

17
env.mjs Normal file
View 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 }

View File

@@ -2,10 +2,6 @@
const nextConfig = { const nextConfig = {
pageExtensions: ['js', 'jsx', 'ts', 'tsx'], pageExtensions: ['js', 'jsx', 'ts', 'tsx'],
reactStrictMode: true, reactStrictMode: true,
experimental: {
// mdxRs: true,
serverActions: true,
}
} }
module.exports = nextConfig; module.exports = nextConfig;

View File

@@ -10,13 +10,13 @@
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.367.0", "@aws-sdk/client-s3": "^3.367.0",
"@sendgrid/mail": "^7.7.0", "@t3-oss/env-nextjs": "^0.7.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"eslint": "^8.46.0", "eslint": "^8.46.0",
"eslint-config-next": "^13.4.12", "eslint-config-next": "^13.4.12",
"next": "^13.4.12", "next": "^13.4.12",
"pg": "^8.11.3", "pg": "^8.11.3",
"postcss": "8.4.24", "postcss": "^8.4.31",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-icons": "^4.9.0", "react-icons": "^4.9.0",
@@ -31,6 +31,7 @@
"@types/react-dom": "18.2.4", "@types/react-dom": "18.2.4",
"@types/uuid": "^9.0.1", "@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" "zod": "^3.22.4"
} }
} }

View File

@@ -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)
})
}

View File

@@ -1,14 +1,35 @@
import { Project, isProject } from "../entities/project"; import { Project, isProject } from "../entities/project";
import { Client } from "pg";
import createClient from "../services/pg"; import createClient from "../services/pg";
export default class ProjectRepository { 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() { async getProjects() {
'use server'; const client = await createClient();
if (!client) return null;
const client = createClient(); const { rows } = await client.query("SELECT * FROM project");
const { rows } = await client.query("SELECT * FROM projects");
await client.end(); await client.end();
if (rows.every(row => isProject(row))) { if (rows.every(row => isProject(row))) {
@@ -19,10 +40,11 @@ export default class ProjectRepository {
} }
async getProjectById(id: string) { async getProjectById(id: string) {
'use server'; const client = await createClient();
if (!client) return null;
await client.connect();
const client = createClient(); const { rows } = await client.query("SELECT * FROM project WHERE id = $1", [id]);
const { rows } = await client.query("SELECT * FROM projects WHERE id = $1", [id]);
await client.end(); await client.end();
if (rows.every(row => isProject(row))) { if (rows.every(row => isProject(row))) {

View File

@@ -1,15 +0,0 @@
export default class WorkActions {
// 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;
// }
}

View File

@@ -3,8 +3,8 @@ import { z } from "zod";
export type Project = { export type Project = {
name: string; name: string;
description: string; description: string;
startDate: Date; created: Date;
endDate?: Date; updated?: Date;
tagIDs?: string[]; tagIDs?: string[];
media?: string[]; // array of URLs media?: string[]; // array of URLs
} }
@@ -19,8 +19,8 @@ export class ProjectCreationError extends Error {
export const ZProject = z.object({ export const ZProject = z.object({
name: z.string(), name: z.string(),
description: z.string(), description: z.string(),
startDate: z.date(), created: z.date(),
endDate: z.date().optional(), updated: z.date().optional(),
tagIDs: z.array(z.string()).optional(), tagIDs: z.array(z.string()).optional(),
media: z.array(z.string()).optional(), media: z.array(z.string()).optional(),
}) })
@@ -34,8 +34,9 @@ export function createProject(data: Partial<Project>) {
const completeInput = { const completeInput = {
name: data.name, name: data.name,
description: data.description, description: data.description,
startDate: data.startDate || today, created: data.created || today,
} updated: today,
} satisfies Partial<Project>;
const parsedProject = ZProject.safeParse(completeInput); const parsedProject = ZProject.safeParse(completeInput);

View File

@@ -1,4 +1,6 @@
import { Client } from "pg"; import { env } from "@/env.mjs";
import pg from 'pg';
const { Client } = pg;
export class PostgresError extends Error { export class PostgresError extends Error {
constructor(message: string) { constructor(message: string) {
@@ -7,13 +9,16 @@ export class PostgresError extends Error {
} }
} }
export default function createClient() { export default async function createClient() {
if (!process.env.POSTGRES_URL) { try {
throw new PostgresError("Database connection configured incorrectly")
}
return new Client({ return new Client({
connectionString: process.env.POSTGRES_URL 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;
}
} }

View File

@@ -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;
}
}