improved setup for server components
This commit is contained in:
@@ -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'm looking forward to hearing from you.</h1>
|
<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 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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 = {
|
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;
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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))) {
|
||||||
|
|||||||
@@ -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 = {
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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