Update MySQL example. (#34784)

Changes from https://github.com/planetscale/nextjs-starter.

Realized the current example was not only out of date, but the demo was broken. Will update the demo site post-merge here 👍
This commit is contained in:
Lee Robinson 2022-02-24 20:41:41 -07:00 committed by GitHub
parent 079b507327
commit fe312ed4bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 326 additions and 807 deletions

View file

@ -0,0 +1 @@
DATABASE_URL=mysql://<USERNAME>:<PLAIN_TEXT_PASSWORD>@<ACCESS_HOST_URL>/<DATABASE_NAME>?sslaccept=strict

View file

@ -1,7 +0,0 @@
# Example .env.local file for MySQL Database credentials
MYSQL_HOST=
MYSQL_DATABASE=
MYSQL_USERNAME=
MYSQL_PASSWORD=
MYSQL_PORT=

View file

@ -1,82 +1,102 @@
# MySQL Example
# Next.js + MySQL
This is an example of using [MySQL](https://www.mysql.com/) in a Next.js project.
This is a [Next.js](https://nextjs.org/) project that uses [Prisma](https://www.prisma.io/) to connect to a [PlanetScale](https://planetscale.com/) MySQL database and [Tailwind CSS](https://tailwindcss.com/) for styling.
## Demo
### [https://next-mysql.vercel.app](https://next-mysql.vercel.app/)
https://next-mysql.vercel.app
## Deploy your own
## Prerequisites
Once you have access to [the environment variables you'll need](#step-5-set-up-environment-variables), deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
- [Node.js](https://nodejs.org/en/download/)
- [PlanetScale CLI](https://github.com/planetscale/cli)
- Authenticate the CLI with the following command:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-mysql&project-name=nextjs-mysql&repository-name=nextjs-mysql&env=MYSQL_HOST,MYSQL_DATABASE,MYSQL_USERNAME,MYSQL_PASSWORD&envDescription=Required%20to%20connect%20the%20app%20with%20MySQL&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-mysql%23step-2-set-up-environment-variables&demo-title=Next.js%20%2B%20MySQL%20Demo&demo-description=A%20simple%20app%20demonstrating%20Next.js%20and%20MySQL%20&demo-url=https%3A%2F%2Fnext-mysql.vercel.app%2F)
```sh
pscale auth login
```
## How to use
## Set up the database
Create a new database with the following command:
```sh
pscale database create <DATABASE_NAME>
```
> A branch, `main`, was automatically created when you created your database, so you can use that for `BRANCH_NAME` in the steps below.
## Set up the starter Next.js app
Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
npx create-next-app --example with-mysql next-mysql-app
npx create-next-app --example with-mysql nextjs-mysql
# or
yarn create next-app --example with-mysql next-mysql-app
yarn create next-app --example with-mysql nextjs-mysql
```
## Configuration
Next, you'll need to create a database username and password through the CLI to connect to your application. If you'd prefer to use the dashboard for this step, you can find those instructions in the [Connection Strings documentation](https://docs.planetscale.com/concepts/connection-strings#creating-a-password) and then come back here to finish setup.
### Step 1. Set up a MySQL database
First, create your `.env` file by renaming the `.env.example` file to `.env`:
Set up a MySQL server either locally or any cloud provider.
### Step 2. Set up environment variables
Copy the `env.local.example` file in this directory to `.env.local` (which will be ignored by Git):
```bash
cp .env.local.example .env.local
```sh
mv .env.example .env
```
Set each variable on `.env.local`:
Next, using the PlanetScale CLI, create a new username and password for the branch of your database:
- `MYSQL_HOST` - Your MySQL host URL.
- `MYSQL_DATABASE` - The name of the MySQL database you want to use.
- `MYSQL_USERNAME` - The name of the MySQL user with access to database.
- `MYSQL_PASSWORD` - The passowrd of the MySQL user.
### Step 3. Run migration script
You'll need to run a migration to create the necessary table for the example.
```bash
npm run migrate
# or
yarn migrate
```sh
pscale password create <DATABASE_NAME> <BRANCH_NAME> <PASSWORD_NAME>
```
### Step 4. Run Next.js in development mode
> The `PASSWORD_NAME` value represents the name of the username and password being generated. You can have multiple credentials for a branch, so this gives you a way to categorize them. To manage your passwords in the dashboard, go to your database overview page, click "Settings", and then click "Passwords".
```bash
npm install
npm run dev
# or
yarn install
yarn dev
Take note of the values returned to you, as you won't be able to see this password again.
```text
Password production-password was successfully created.
Please save the values below as they will not be shown again
NAME USERNAME ACCESS HOST URL ROLE PLAIN TEXT
--------------------- -------------- ----------------------------------- ------------------ -------------------------------------------------------
production-password xxxxxxxxxxxxx xxxxxx.us-east-2.psdb.cloud Can Read & Write pscale_pw_xxxxxxx
```
Your app should be up and running on [http://localhost:3000](http://localhost:3000)! If it doesn't work, post on [GitHub discussions](https://github.com/vercel/next.js/discussions).
You'll use these properties to construct your connection string, which will be the value for `DATABASE_URL` in your `.env` file. Update the `DATABASE_URL` property with your connection string in the following format:
## Deploy on Vercel
```text
mysql://<USERNAME>:<PLAIN_TEXT_PASSWORD>@<ACCESS_HOST_URL>/<DATABASE_NAME>?sslaccept=strict
```
You can deploy this app to the cloud with [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
Push the database schema to your PlanetScale database using Prisma.
#### Deploy Your Local Project
`npx prisma db push`
To deploy your local project to Vercel, push it to GitHub/GitLab/Bitbucket and [import to Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example).
Run the seed script to populate your database with `Product` and `Category` data.
**Important**: When you import your project on Vercel, make sure to click on **Environment Variables** and set them to match your `.env.local` file.
`npx run seed`
#### Deploy from Our Template
## Run the App
Alternatively, you can deploy using our template by clicking on the Deploy button below.
Run the app with following command:
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-mysql&project-name=nextjs-mysql&repository-name=nextjs-mysql&env=MYSQL_HOST,MYSQL_DATABASE,MYSQL_USERNAME,MYSQL_PASSWORD&envDescription=Required%20to%20connect%20the%20app%20with%20MySQL&envLink=https%3A%2F%2Fgithub.com%2Fvercel%2Fnext.js%2Ftree%2Fcanary%2Fexamples%2Fwith-mysql%23step-2-set-up-environment-variables&demo-title=Next.js%20%2B%20MySQL%20Demo&demo-description=A%20simple%20app%20demonstrating%20Next.js%20and%20MySQL%20&demo-url=https%3A%2F%2Fnext-mysql.vercel.app%2F)
`npm run dev`
Open your browser at [localhost:3000](localhost:3000) to see the running application.
## Deploy your own
After you've got your application running locally, it's time to deploy it. To do so, you'll need to promote your database branch (`main` by default) to be the production branch ([read the branching documentation for more information](https://docs.planetscale.com/concepts/branching)).
```sh
pscale branch promote <DATABASE_NAME> <BRANCH_NAME>
```
Now that your branch has been promoted to production, you can either use the existing password you generated earlier for running locally or create a new password. Regardless, you'll need a password in the deployment steps below.
Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example):
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-mysql&project-name=with-mysql&repository-name=with-mysql&env=DATABASE_URL)
> Make sure to update the `DATABASE_URL` variable during this setup process.

View file

@ -0,0 +1,30 @@
import Image from 'next/image'
export default function Product({ product }) {
const { name, description, price, image, category } = product
return (
<div
className="max-w-xs rounded overflow-hidden shadow-lg"
key={product.id}
>
<Image
className="w-full"
width={320}
height={160}
src={image}
alt={name}
/>
<div className="px-6 py-4">
<div className="font-bold text-xl mb-2">{name}</div>
<p className="text-gray-700 text-base">{description}</p>
<p className="text-gray-900 text-xl">${price}</p>
</div>
<div className="px-6 pt-4 pb-2">
<span className="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2 mb-2">
{category.name}
</span>
</div>
</div>
)
}

View file

@ -1,27 +0,0 @@
import Link from 'next/link'
import cn from 'clsx'
function ButtonLink({ href = '/', className = '', children }) {
return (
<Link href={href}>
<a
className={cn(
'bg-black',
'text-white',
'p-2',
'rounded',
'uppercase',
'text-sm',
'font-bold',
{
[className]: Boolean(className),
}
)}
>
{children}
</a>
</Link>
)
}
export default ButtonLink

View file

@ -1,33 +0,0 @@
import cn from 'clsx'
function Button({
onClick = console.log,
className = '',
children = null,
type = null,
disabled = false,
}) {
return (
<button
type={type}
onClick={onClick}
disabled={disabled}
className={cn(
'bg-black',
'text-white',
'p-2',
'rounded',
'uppercase',
'text-sm',
'font-bold',
{
[className]: Boolean(className),
}
)}
>
{children}
</button>
)
}
export default Button

View file

@ -1,5 +0,0 @@
function Container({ className = '', children }) {
return <div className={'container mx-auto px-8 ' + className}>{children}</div>
}
export default Container

View file

@ -1,78 +0,0 @@
import { useState, useEffect } from 'react'
import Router, { useRouter } from 'next/router'
import Button from '../button'
export default function EntryForm() {
const [_title, setTitle] = useState('')
const [_content, setContent] = useState('')
const [submitting, setSubmitting] = useState(false)
const router = useRouter()
const { id, title, content } = router.query
useEffect(() => {
if (typeof title === 'string') {
setTitle(title)
}
if (typeof content === 'string') {
setContent(content)
}
}, [title, content])
async function submitHandler(e) {
e.preventDefault()
setSubmitting(true)
try {
const res = await fetch('/api/edit-entry', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id,
title: _title,
content: _content,
}),
})
const json = await res.json()
setSubmitting(false)
if (!res.ok) throw Error(json.message)
Router.push('/')
} catch (e) {
throw Error(e.message)
}
}
return (
<form onSubmit={submitHandler}>
<div className="my-4">
<label htmlFor="title">
<h3 className="font-bold">Title</h3>
</label>
<input
id="title"
className="shadow border rounded w-full"
type="text"
name="title"
value={_title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div className="my-4">
<label htmlFor="content">
<h3 className="font-bold">Content</h3>
</label>
<textarea
className="shadow border resize-none focus:shadow-outline w-full h-48"
id="content"
name="content"
value={_content}
onChange={(e) => setContent(e.target.value)}
/>
</div>
<Button disabled={submitting} type="submit">
{submitting ? 'Saving ...' : 'Save'}
</Button>
</form>
)
}

View file

@ -1,46 +0,0 @@
import { useState } from 'react'
import Link from 'next/link'
import { mutate } from 'swr'
import ButtonLink from '@/components/button-link'
import Button from '@/components/button'
function Entry({ id, title, content }) {
const [deleting, setDeleting] = useState(false)
async function deleteEntry() {
setDeleting(true)
let res = await fetch(`/api/delete-entry?id=${id}`, { method: 'DELETE' })
let json = await res.json()
if (!res.ok) throw Error(json.message)
mutate('/api/get-entries')
setDeleting(false)
}
return (
<div>
<div className="flex items-center">
<Link href={`/entry/${id}`}>
<a className="font-bold py-2">{title}</a>
</Link>
<div className="flex ml-4">
<ButtonLink
href={`/entry/edit/${id}?title=${title}&content=${content}`}
className="h-5 py-0 mx-1"
>
Edit
</ButtonLink>
<Button
disabled={deleting}
onClick={deleteEntry}
className="h-5 py-0 mx-1"
>
{deleting ? 'Deleting ...' : 'Delete'}
</Button>
</div>
</div>
<p>{content}</p>
</div>
)
}
export default Entry

View file

@ -1,19 +0,0 @@
import Entry from './entry'
function Entries({ entries }) {
if (entries) {
return (
<div>
{entries.map((e) => (
<div key={e.id} className="py-2">
<Entry id={e.id} title={e.title} content={e.content} />
</div>
))}
</div>
)
} else {
return null
}
}
export default Entries

View file

@ -1,66 +0,0 @@
import { useState } from 'react'
import Router from 'next/router'
import Button from '@/components/button'
export default function EntryForm() {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [submitting, setSubmitting] = useState(false)
async function submitHandler(e) {
setSubmitting(true)
e.preventDefault()
try {
const res = await fetch('/api/create-entry', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
content,
}),
})
setSubmitting(false)
const json = await res.json()
if (!res.ok) throw Error(json.message)
Router.push('/')
} catch (e) {
throw Error(e.message)
}
}
return (
<form onSubmit={submitHandler}>
<div className="my-4">
<label htmlFor="title">
<h3 className="font-bold">Title</h3>
</label>
<input
id="title"
className="shadow border rounded w-full"
type="text"
name="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div className="my-4">
<label htmlFor="content">
<h3 className="font-bold">Content</h3>
</label>
<textarea
className="shadow border resize-none focus:shadow-outline w-full h-48"
id="content"
name="content"
value={content}
onChange={(e) => setContent(e.target.value)}
/>
</div>
<Button disabled={submitting} type="submit">
{submitting ? 'Creating ...' : 'Create'}
</Button>
</form>
)
}

View file

@ -1,13 +0,0 @@
import PoweredByVercel from '../powered-by-vercel'
function Footer() {
return (
<footer className="container mx-auto fixed bottom-0 right-0 left-0 flex justify-center items-center h-24">
<a href="https://vercel.com?utm_source=next-mysql">
<PoweredByVercel />
</a>
</footer>
)
}
export default Footer

View file

@ -1,18 +0,0 @@
import Link from 'next/link'
import Container from '@/components/container'
import ButtonLink from '@/components/button-link'
export default function Nav({ title = 'Entries' }) {
return (
<Container className="py-4">
<nav>
<div className="flex justify-between items-center">
<Link href="/">
<a className="font-bold text-3xl">{title}</a>
</Link>
<ButtonLink href="/new">New Entry</ButtonLink>
</div>
</nav>
</Container>
)
}

File diff suppressed because one or more lines are too long

View file

@ -1,24 +0,0 @@
import mysql from 'serverless-mysql'
export const db = mysql({
config: {
host: process.env.MYSQL_HOST,
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USERNAME,
password: process.env.MYSQL_PASSWORD,
port: parseInt(process.env.MYSQL_PORT),
},
})
export async function query(
q: string,
values: (string | number)[] | string | number = []
) {
try {
const results = await db.query(q, values)
await db.end()
return results
} catch (e) {
throw Error(e.message)
}
}

View file

@ -0,0 +1,14 @@
import { PrismaClient } from '@prisma/client'
let prisma
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient()
} else {
if (!global.prisma) {
global.prisma = new PrismaClient()
}
prisma = global.prisma
}
export default prisma

View file

@ -1,19 +0,0 @@
import useSWR from 'swr'
function fetcher(url: string) {
return window.fetch(url).then((res) => res.json())
}
export function useEntries() {
const { data, error } = useSWR(`/api/get-entries`, fetcher)
return {
entries: data,
isLoading: !error && !data,
isError: error,
}
}
export function useEntry(id: string) {
return useSWR(`/api/get-entry?id=${id}`, fetcher)
}

View file

@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View file

@ -0,0 +1,3 @@
module.exports = {
reactStrictMode: true,
}

View file

@ -1,28 +1,21 @@
{
"private": true,
"scripts": {
"dev": "next",
"dev": "next dev",
"build": "next build",
"start": "next start",
"migrate": "node scripts/migrate-db.js"
"seed": "node prisma/seed.js"
},
"dependencies": {
"bad-words": "^3.0.4",
"clsx": "^1.1.1",
"dotenv": "^10.0.0",
"@prisma/client": "3.10.0",
"next": "latest",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-loading-skeleton": "^2.1.1",
"serverless-mysql": "^1.5.4",
"swr": "^0.3.9"
"react": "17.0.2",
"react-dom": "17.0.2"
},
"devDependencies": {
"@types/node": "^14.14.6",
"@types/react": "^16.9.55",
"postcss-flexbugs-fixes": "4.2.1",
"postcss-preset-env": "^6.7.0",
"tailwindcss": "^1.7.4",
"typescript": "^4.0.5"
"autoprefixer": "10.4.2",
"postcss": "8.4.7",
"prisma": "3.10.0",
"tailwindcss": "3.0.23"
}
}

View file

@ -0,0 +1,5 @@
import '../styles/globals.css'
export default function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}

View file

@ -1,13 +0,0 @@
import '../styles/index.css'
import Footer from '@/components/footer'
function MyApp({ Component, pageProps }) {
return (
<>
<Component {...pageProps} />
<Footer />
</>
)
}
export default MyApp

View file

@ -1,30 +0,0 @@
import { NextApiHandler } from 'next'
import Filter from 'bad-words'
import { query } from '../../lib/db'
const filter = new Filter()
const handler: NextApiHandler = async (req, res) => {
const { title, content } = req.body
try {
if (!title || !content) {
return res
.status(400)
.json({ message: '`title` and `content` are both required' })
}
const results = await query(
`
INSERT INTO entries (title, content)
VALUES (?, ?)
`,
[filter.clean(title), filter.clean(content)]
)
return res.json(results)
} catch (e) {
res.status(500).json({ message: e.message })
}
}
export default handler

View file

@ -1,26 +0,0 @@
import { NextApiHandler } from 'next'
import { query } from '../../lib/db'
const handler: NextApiHandler = async (req, res) => {
const { id } = req.query
try {
if (!id) {
return res.status(400).json({ message: '`id` required' })
}
if (typeof parseInt(id.toString()) !== 'number') {
return res.status(400).json({ message: '`id` must be a number' })
}
const results = await query(
`
DELETE FROM entries
WHERE id = ?
`,
id
)
res.json(results)
} catch (e) {
res.status(500).json({ message: e.message })
}
}
export default handler

View file

@ -1,31 +0,0 @@
import { NextApiHandler } from 'next'
import Filter from 'bad-words'
import { query } from '../../lib/db'
const filter = new Filter()
const handler: NextApiHandler = async (req, res) => {
const { id, title, content } = req.body
try {
if (!id || !title || !content) {
return res
.status(400)
.json({ message: '`id`,`title`, and `content` are all required' })
}
const results = await query(
`
UPDATE entries
SET title = ?, content = ?
WHERE id = ?
`,
[filter.clean(title), filter.clean(content), id]
)
return res.json(results)
} catch (e) {
res.status(500).json({ message: e.message })
}
}
export default handler

View file

@ -1,18 +0,0 @@
import { NextApiHandler } from 'next'
import { query } from '../../lib/db'
const handler: NextApiHandler = async (_, res) => {
try {
const results = await query(`
SELECT * FROM entries
ORDER BY id DESC
LIMIT 10
`)
return res.json(results)
} catch (e) {
res.status(500).json({ message: e.message })
}
}
export default handler

View file

@ -1,28 +0,0 @@
import { NextApiHandler } from 'next'
import { query } from '../../lib/db'
const handler: NextApiHandler = async (req, res) => {
const { id } = req.query
try {
if (!id) {
return res.status(400).json({ message: '`id` required' })
}
if (typeof parseInt(id.toString()) !== 'number') {
return res.status(400).json({ message: '`id` must be a number' })
}
const results = await query(
`
SELECT id, title, content
FROM entries
WHERE id = ?
`,
id
)
return res.json(results[0])
} catch (e) {
res.status(500).json({ message: e.message })
}
}
export default handler

View file

@ -0,0 +1,16 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import prisma from './../../lib/prisma.js'
export default async function handler(req, res) {
if (req.method === 'GET') {
try {
const data = await prisma.product.findMany({})
return res.status(200).json({ data })
} catch (err) {
console.error(err)
return res.status(500).json({ msg: 'Something went wrong' })
}
} else {
return res.status(405).json({ msg: 'Method not allowed' })
}
}

View file

@ -1,33 +0,0 @@
import { useRouter } from 'next/router'
import { useEntry } from '@/lib/swr-hooks'
import Container from '@/components/container'
import Nav from '@/components/nav'
export default function EditEntryPage() {
const router = useRouter()
const id = router.query.id?.toString()
const { data } = useEntry(id)
if (data) {
return (
<>
<Nav title="View" />
<Container>
<h1 className="font-bold text-3xl my-2">{data.title}</h1>
<p>{data.content}</p>
</Container>
</>
)
} else {
return (
<>
<Nav title="View" />
<Container>
<h1 className="font-bold text-3xl my-2">...</h1>
<p>...</p>
</Container>
</>
)
}
}

View file

@ -1,14 +0,0 @@
import Container from '@/components/container'
import Nav from '@/components/nav'
import EditEntryForm from '@/components/edit-entry-form'
export default function EditEntryPage() {
return (
<>
<Nav title="Edit" />
<Container>
<EditEntryForm />
</Container>
</>
)
}

View file

@ -0,0 +1,46 @@
import Head from 'next/head'
import Product from '../components/Product'
import prisma from '../lib/prisma'
export default function Home({ products }) {
return (
<div>
<Head>
<title>PlanetScale Next.js Quickstart</title>
<meta name="description" content="PlanetScale Quickstart for Next.js" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="p-10 mx-auto max-w-4xl">
<h1 className="text-6xl font-bold mb-4 text-center">Next.js Starter</h1>
<p className="mb-20 text-xl text-center">
🔥 Shop from the hottest items in the world 🔥
</p>
<div className="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 justify-items-center gap-4">
{products.map((product) => (
<Product product={product} key={product.id} />
))}
</div>
</main>
<footer></footer>
</div>
)
}
export async function getStaticProps(context) {
const data = await prisma.product.findMany({
include: {
category: true,
},
})
//convert decimal value to string to pass through as json
const products = data.map((product) => ({
...product,
price: product.price.toString(),
}))
return {
props: { products },
}
}

View file

@ -1,38 +0,0 @@
import Skeleton from 'react-loading-skeleton'
import Nav from '@/components/nav'
import Container from '@/components/container'
import Entries from '@/components/entries'
import { useEntries } from '@/lib/swr-hooks'
export default function IndexPage() {
const { entries, isLoading } = useEntries()
if (isLoading) {
return (
<div>
<Nav />
<Container>
<Skeleton width={180} height={24} />
<Skeleton height={48} />
<div className="my-4" />
<Skeleton width={180} height={24} />
<Skeleton height={48} />
<div className="my-4" />
<Skeleton width={180} height={24} />
<Skeleton height={48} />
</Container>
</div>
)
}
return (
<div>
<Nav />
<Container>
<Entries entries={entries} />
</Container>
</div>
)
}

View file

@ -1,14 +0,0 @@
import Nav from '@/components/nav'
import Container from '@/components/container'
import EntryForm from '@/components/entry-form'
export default function NewEntryPage() {
return (
<>
<Nav title="New" />
<Container className="w-full lg:w-2/4">
<EntryForm />
</Container>
</>
)
}

View file

@ -1,18 +1,6 @@
module.exports = {
plugins: [
'tailwindcss',
'postcss-flexbugs-fixes',
[
'postcss-preset-env',
{
autoprefixer: {
flexbox: 'no-2009',
},
stage: 3,
features: {
'custom-properties': false,
},
},
],
],
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -0,0 +1,52 @@
const { Prisma } = require('@prisma/client')
const categories = [
{
name: 'Hats',
description: 'Things you can wear on your head',
},
{
name: 'Socks',
description: 'Things you can wear on your feet',
},
{
name: 'Shirts',
description: 'Things you wear on the top half of your body',
},
]
const products = [
{
name: 'Cool hat.',
description: 'A nice hat to wear on your head',
price: new Prisma.Decimal(19.95),
image: '/images/placeholder.jpg',
category_id: 1,
},
{
name: 'Grey T-Shirt',
description: 'A nice shirt that you can wear on your body',
price: new Prisma.Decimal(22.95),
image: '/images/placeholder.jpg',
category_id: 3,
},
{
name: 'Socks',
description: 'Cool socks that you can wear on your feet',
price: new Prisma.Decimal(12.95),
image: '/images/placeholder.jpg',
category_id: 2,
},
{
name: 'Sweatshirt',
description: 'Cool sweatshirt that you can wear on your body',
price: new Prisma.Decimal(12.95),
image: '/images/placeholder.jpg',
category_id: 3,
},
]
module.exports = {
products,
categories,
}

View file

@ -0,0 +1,29 @@
generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
referentialIntegrity = "prisma"
}
model Product {
id Int @id @default(autoincrement())
name String
description String
price Decimal
image String
category Category? @relation(fields: [category_id], references: [id])
category_id Int
@@index([category_id])
}
model Category {
id Int @id @default(autoincrement())
name String
description String
products Product[]
}

View file

@ -0,0 +1,36 @@
const { PrismaClient } = require('@prisma/client')
const { categories, products } = require('./data.js')
const prisma = new PrismaClient()
const load = async () => {
try {
await prisma.category.deleteMany()
console.log('Deleted records in category table')
await prisma.product.deleteMany()
console.log('Deleted records in product table')
await prisma.$queryRaw`ALTER TABLE Product AUTO_INCREMENT = 1`
console.log('reset product auto increment to 1')
await prisma.$queryRaw`ALTER TABLE Category AUTO_INCREMENT = 1`
console.log('reset category auto increment to 1')
await prisma.category.createMany({
data: categories,
})
console.log('Added category data')
await prisma.product.createMany({
data: products,
})
console.log('Added product data')
} catch (e) {
console.error(e)
process.exit(1)
} finally {
await prisma.$disconnect()
}
}
load()

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,53 +0,0 @@
const path = require('path')
const envPath = path.resolve(process.cwd(), '.env.local')
console.log({ envPath })
require('dotenv').config({ path: envPath })
const mysql = require('serverless-mysql')
const db = mysql({
config: {
host: process.env.MYSQL_HOST,
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USERNAME,
password: process.env.MYSQL_PASSWORD,
port: process.env.MYSQL_PORT,
},
})
async function query(q) {
try {
const results = await db.query(q)
await db.end()
return results
} catch (e) {
throw Error(e.message)
}
}
// Create "entries" table if doesn't exist
async function migrate() {
try {
await query(`
CREATE TABLE IF NOT EXISTS entries (
id INT AUTO_INCREMENT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at
TIMESTAMP
NOT NULL
DEFAULT CURRENT_TIMESTAMP
ON UPDATE CURRENT_TIMESTAMP
)
`)
console.log('migration ran successfully')
} catch (e) {
console.error('could not run migration, double check your credentials.')
process.exit(1)
}
}
migrate().then(() => process.exit())

View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -1,18 +0,0 @@
@tailwind base;
/* Write your own custom base styles here */
/* Start purging... */
@tailwind components;
/* Stop purging. */
/* Write your own custom component styles here */
.btn-blue {
@apply bg-blue-500 text-white font-bold py-2 px-4 rounded;
}
/* Start purging... */
@tailwind utilities;
/* Stop purging. */
/* Your own custom utilities */

View file

@ -1,16 +1,10 @@
module.exports = {
future: {
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
},
purge: ['./components/**/*.{js,ts,jsx,tsx}', './pages/**/*.{js,ts,jsx,tsx}'],
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
colors: {
'accent-1': '#333',
},
},
extend: {},
},
variants: {},
plugins: [],
}

View file

@ -1,24 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/components/*": ["components/*"],
"@/lib/*": ["lib/*"]
},
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}