improved setup for server components
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
'use client';
|
||||
import { 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,30 +1,25 @@
|
||||
import Image from "next/image";
|
||||
// 'use client';
|
||||
import ProjectRepository from "@/server/actions/project.actions";
|
||||
import Image from "next/image";
|
||||
|
||||
interface PageProps {
|
||||
searchParams: any;
|
||||
params: {
|
||||
id: any;
|
||||
}
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function ProjectById(props: PageProps) {
|
||||
const { id } = props.params;
|
||||
|
||||
export default async function ProjectById(req: { params: any, searchParams: any }) {
|
||||
const projects = new ProjectRepository();
|
||||
const project = await projects.getProjectById(id);
|
||||
const project = await projects.getProjectById(req.params.id);
|
||||
|
||||
if (!project) {
|
||||
return <p>Project not found!</p>
|
||||
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.startDate.toLocaleString()}</p>
|
||||
<p>{project.endDate ? `Finished: ${project.endDate.toLocaleDateString()}` : "(In progress)"}</p> */}
|
||||
<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">
|
||||
|
||||
@@ -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
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;
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.367.0",
|
||||
"@sendgrid/mail": "^7.7.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",
|
||||
"pg": "^8.11.3",
|
||||
"postcss": "8.4.24",
|
||||
"postcss": "^8.4.31",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "^4.9.0",
|
||||
@@ -31,6 +31,7 @@
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vercel/style-guide": "^5.0.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"zod": "^3.22.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -1,14 +1,35 @@
|
||||
import { Project, isProject } from "../entities/project";
|
||||
import { Client } from "pg";
|
||||
import createClient from "../services/pg";
|
||||
|
||||
export default class ProjectRepository {
|
||||
async getProjects() {
|
||||
'use server';
|
||||
|
||||
const client = createClient();
|
||||
async createProject(data: Project) {
|
||||
const client = await createClient();
|
||||
if (!client) return null;
|
||||
await client.connect();
|
||||
|
||||
const { rows } = await client.query("SELECT * FROM projects");
|
||||
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))) {
|
||||
@@ -19,10 +40,11 @@ export default class ProjectRepository {
|
||||
}
|
||||
|
||||
async getProjectById(id: string) {
|
||||
'use server';
|
||||
|
||||
const client = createClient();
|
||||
const { rows } = await client.query("SELECT * FROM projects WHERE id = $1", [id]);
|
||||
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))) {
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
}
|
||||
@@ -3,8 +3,8 @@ import { z } from "zod";
|
||||
export type Project = {
|
||||
name: string;
|
||||
description: string;
|
||||
startDate: Date;
|
||||
endDate?: Date;
|
||||
created: Date;
|
||||
updated?: Date;
|
||||
tagIDs?: string[];
|
||||
media?: string[]; // array of URLs
|
||||
}
|
||||
@@ -19,8 +19,8 @@ export class ProjectCreationError extends Error {
|
||||
export const ZProject = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
startDate: z.date(),
|
||||
endDate: z.date().optional(),
|
||||
created: z.date(),
|
||||
updated: z.date().optional(),
|
||||
tagIDs: z.array(z.string()).optional(),
|
||||
media: z.array(z.string()).optional(),
|
||||
})
|
||||
@@ -34,8 +34,9 @@ export function createProject(data: Partial<Project>) {
|
||||
const completeInput = {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
startDate: data.startDate || today,
|
||||
}
|
||||
created: data.created || today,
|
||||
updated: today,
|
||||
} satisfies Partial<Project>;
|
||||
|
||||
const parsedProject = ZProject.safeParse(completeInput);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
constructor(message: string) {
|
||||
@@ -7,13 +9,16 @@ export class PostgresError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export default function createClient() {
|
||||
if (!process.env.POSTGRES_URL) {
|
||||
throw new PostgresError("Database connection configured incorrectly")
|
||||
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;
|
||||
}
|
||||
|
||||
return new Client({
|
||||
connectionString: process.env.POSTGRES_URL
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user