├── app
├── favicon.ico
├── api
│ ├── post
│ │ └── [id]
│ │ │ └── route.js
│ └── add-post
│ │ └── route.js
├── components
│ ├── Post.jsx
│ └── DeletePostButton.jsx
├── layout.js
├── page.jsx
├── add-post
│ └── page.jsx
├── globals.css
└── page.module.css
├── jsconfig.json
├── next.config.js
├── lib
└── prisma.js
├── package.json
├── .gitignore
├── public
├── vercel.svg
└── next.svg
├── schema.prisma
└── README.md
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/coderyansolomon/fullstack-intro/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "paths": {
4 | "@/*": ["./*"]
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {}
3 |
4 | module.exports = nextConfig
5 |
--------------------------------------------------------------------------------
/app/api/post/[id]/route.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import prisma from "@/lib/prisma";
3 |
4 | export async function DELETE(request, {params}){
5 | const id = params.id;
6 |
7 | const post = await prisma.post.delete({
8 | where: {id}
9 | })
10 |
11 | return NextResponse.json(post)
12 | }
--------------------------------------------------------------------------------
/lib/prisma.js:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 |
3 | let prisma;
4 |
5 | if (process.env.NODE_ENV === 'production'){
6 | prisma = new PrismaClient()
7 | } else {
8 | if (!global.prisma){
9 | global.prisma = new PrismaClient()
10 | }
11 | prisma = global.prisma
12 | }
13 |
14 | export default prisma;
--------------------------------------------------------------------------------
/app/components/Post.jsx:
--------------------------------------------------------------------------------
1 | import DeletePostButton from "./DeletePostButton";
2 |
3 | export default function Post({id, title, content, authorName}){
4 | return (
5 |
6 |
{authorName}
7 |
{title}
8 |
{content}
9 |
10 |
11 | )
12 | }
--------------------------------------------------------------------------------
/app/layout.js:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import { Inter } from 'next/font/google'
3 |
4 | const inter = Inter({ subsets: ['latin'] })
5 |
6 | export const metadata = {
7 | title: 'Create Next App',
8 | description: 'Generated by create next app',
9 | }
10 |
11 | export default function RootLayout({ children }) {
12 | return (
13 |
14 | {children}
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "full-stack-intro",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "postinstall": "prisma generate"
11 | },
12 | "dependencies": {
13 | "@prisma/client": "^5.1.1",
14 | "next": "13.4.16",
15 | "react": "18.2.0",
16 | "react-dom": "18.2.0"
17 | },
18 | "devDependencies": {
19 | "prisma": "^5.1.1"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 | .env
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/app/api/add-post/route.js:
--------------------------------------------------------------------------------
1 | import prisma from "@/lib/prisma";
2 | import { NextResponse } from "next/server"
3 |
4 | export async function POST(request){
5 | const res = await request.json()
6 | const {title, content} = res;
7 | const result = await prisma.post.create({
8 | data: {
9 | title,
10 | content,
11 | published: true,
12 | author: {create: {
13 | name: 'ryan'
14 | }}
15 | }
16 | })
17 |
18 | return NextResponse.json({result})
19 | }
--------------------------------------------------------------------------------
/app/components/DeletePostButton.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useRouter } from "next/navigation"
3 |
4 | export default function DeletePostButton({postId}){
5 | const router = useRouter()
6 |
7 | async function handleClick(){
8 |
9 | try {
10 | await fetch(`/api/post/${postId}`, {
11 | method: 'DELETE'
12 | })
13 | router.refresh()
14 | } catch(e){
15 | console.error(e)
16 | }
17 |
18 | }
19 |
20 | return (
21 |
22 | )
23 | }
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/schema.prisma:
--------------------------------------------------------------------------------
1 | // schema.prisma
2 |
3 | generator client {
4 | provider = "prisma-client-js"
5 | previewFeatures = ["jsonProtocol"]
6 | }
7 |
8 | datasource db {
9 | provider = "postgresql"
10 | url = env("POSTGRES_PRISMA_URL") // uses connection pooling
11 | directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
12 | }
13 |
14 | model Post {
15 | id String @default(cuid()) @id
16 | title String
17 | content String?
18 | published Boolean @default(false)
19 | author User? @relation(fields: [authorId], references: [id])
20 | authorId String?
21 | }
22 |
23 | model User {
24 | id String @default(cuid()) @id
25 | name String?
26 | email String? @unique
27 | createdAt DateTime @default(now()) @map(name: "created_at")
28 | updatedAt DateTime @updatedAt @map(name: "updated_at")
29 | posts Post[]
30 | @@map(name: "users")
31 | }
--------------------------------------------------------------------------------
/app/page.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import Post from './components/Post';
3 | import styles from './page.module.css'
4 | import prisma from '@/lib/prisma'
5 |
6 | async function getPosts(){
7 | const posts = await prisma.post.findMany({
8 | where: {published: true},
9 | include: {
10 | author: {
11 | select: {name: true}
12 | }
13 | }
14 | })
15 | return posts;
16 | }
17 |
18 | export default async function Home() {
19 | const posts = await getPosts();
20 | return (
21 |
22 | Add Post
23 | Feed
24 | {
25 | posts.map((post) => {
26 | return (
27 |
34 | )
35 | })
36 | }
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
2 |
3 | ## Getting Started
4 |
5 | First, run the development server:
6 |
7 | ```bash
8 | npm run dev
9 | # or
10 | yarn dev
11 | # or
12 | pnpm dev
13 | ```
14 |
15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
16 |
17 | You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
18 |
19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
20 |
21 | ## Learn More
22 |
23 | To learn more about Next.js, take a look at the following resources:
24 |
25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
27 |
28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
29 |
30 | ## Deploy on Vercel
31 |
32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
33 |
34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
35 |
--------------------------------------------------------------------------------
/app/add-post/page.jsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import styles from '@/app/page.module.css'
3 | import Link from 'next/link';
4 | import { useState } from 'react';
5 | import { useRouter } from 'next/navigation';
6 |
7 | export default function AddPost(){
8 | const [title, setTitle] = useState('');
9 | const [content, setContent] = useState('');
10 | const router = useRouter()
11 |
12 | const handleTitleChange = (event) => {
13 | setTitle(event.target.value);
14 | };
15 |
16 | const handleContentChange = (event) => {
17 | setContent(event.target.value);
18 | };
19 |
20 | const handleSubmit = async (event) => {
21 | event.preventDefault();
22 |
23 | try{
24 | await fetch('/api/add-post', {
25 | method: 'POST',
26 | headers: {
27 | 'Content-Type': 'application/json'
28 | },
29 | body: JSON.stringify({title, content}) })
30 |
31 | router.refresh()
32 | } catch (error){
33 | console.error(error)
34 | }
35 |
36 | setTitle('');
37 | setContent('');
38 | };
39 |
40 | return (
41 |
42 | View Feed
43 | Add Post
44 |
66 |
67 | )
68 | }
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --max-width: 1100px;
3 | --border-radius: 12px;
4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
7 |
8 | --foreground-rgb: 0, 0, 0;
9 | --background-start-rgb: 214, 219, 220;
10 | --background-end-rgb: 255, 255, 255;
11 |
12 | --primary-glow: conic-gradient(
13 | from 180deg at 50% 50%,
14 | #16abff33 0deg,
15 | #0885ff33 55deg,
16 | #54d6ff33 120deg,
17 | #0071ff33 160deg,
18 | transparent 360deg
19 | );
20 | --secondary-glow: radial-gradient(
21 | rgba(255, 255, 255, 1),
22 | rgba(255, 255, 255, 0)
23 | );
24 |
25 | --tile-start-rgb: 239, 245, 249;
26 | --tile-end-rgb: 228, 232, 233;
27 | --tile-border: conic-gradient(
28 | #00000080,
29 | #00000040,
30 | #00000030,
31 | #00000020,
32 | #00000010,
33 | #00000010,
34 | #00000080
35 | );
36 |
37 | --callout-rgb: 238, 240, 241;
38 | --callout-border-rgb: 172, 175, 176;
39 | --card-rgb: 180, 185, 188;
40 | --card-border-rgb: 131, 134, 135;
41 | }
42 |
43 | @media (prefers-color-scheme: dark) {
44 | :root {
45 | --foreground-rgb: 255, 255, 255;
46 | --background-start-rgb: 0, 0, 0;
47 | --background-end-rgb: 0, 0, 0;
48 |
49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
50 | --secondary-glow: linear-gradient(
51 | to bottom right,
52 | rgba(1, 65, 255, 0),
53 | rgba(1, 65, 255, 0),
54 | rgba(1, 65, 255, 0.3)
55 | );
56 |
57 | --tile-start-rgb: 2, 13, 46;
58 | --tile-end-rgb: 2, 5, 19;
59 | --tile-border: conic-gradient(
60 | #ffffff80,
61 | #ffffff40,
62 | #ffffff30,
63 | #ffffff20,
64 | #ffffff10,
65 | #ffffff10,
66 | #ffffff80
67 | );
68 |
69 | --callout-rgb: 20, 20, 20;
70 | --callout-border-rgb: 108, 108, 108;
71 | --card-rgb: 100, 100, 100;
72 | --card-border-rgb: 200, 200, 200;
73 | }
74 | }
75 |
76 | * {
77 | box-sizing: border-box;
78 | padding: 0;
79 | margin: 0;
80 | }
81 |
82 | html,
83 | body {
84 | max-width: 100vw;
85 | overflow-x: hidden;
86 | }
87 |
88 | body {
89 | color: rgb(var(--foreground-rgb));
90 | background: linear-gradient(
91 | to bottom,
92 | transparent,
93 | rgb(var(--background-end-rgb))
94 | )
95 | rgb(var(--background-start-rgb));
96 | }
97 |
98 | a {
99 | color: inherit;
100 | text-decoration: none;
101 | }
102 |
103 | @media (prefers-color-scheme: dark) {
104 | html {
105 | color-scheme: dark;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/app/page.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | padding: 6rem;
7 | min-height: 100vh;
8 | }
9 |
10 | .description {
11 | display: inherit;
12 | justify-content: inherit;
13 | align-items: inherit;
14 | font-size: 0.85rem;
15 | max-width: var(--max-width);
16 | width: 100%;
17 | z-index: 2;
18 | font-family: var(--font-mono);
19 | }
20 |
21 | .description a {
22 | display: flex;
23 | justify-content: center;
24 | align-items: center;
25 | gap: 0.5rem;
26 | }
27 |
28 | .description p {
29 | position: relative;
30 | margin: 0;
31 | padding: 1rem;
32 | background-color: rgba(var(--callout-rgb), 0.5);
33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3);
34 | border-radius: var(--border-radius);
35 | }
36 |
37 | .code {
38 | font-weight: 700;
39 | font-family: var(--font-mono);
40 | }
41 |
42 | .grid {
43 | display: grid;
44 | grid-template-columns: repeat(4, minmax(25%, auto));
45 | max-width: 100%;
46 | width: var(--max-width);
47 | }
48 |
49 | .card {
50 | padding: 1rem 1.2rem;
51 | border-radius: var(--border-radius);
52 | background: rgba(var(--card-rgb), 0);
53 | border: 1px solid rgba(var(--card-border-rgb), 0);
54 | transition: background 200ms, border 200ms;
55 | }
56 |
57 | .card span {
58 | display: inline-block;
59 | transition: transform 200ms;
60 | }
61 |
62 | .card h2 {
63 | font-weight: 600;
64 | margin-bottom: 0.7rem;
65 | }
66 |
67 | .card p {
68 | margin: 0;
69 | opacity: 0.6;
70 | font-size: 0.9rem;
71 | line-height: 1.5;
72 | max-width: 30ch;
73 | }
74 |
75 | .center {
76 | display: flex;
77 | justify-content: center;
78 | align-items: center;
79 | position: relative;
80 | padding: 4rem 0;
81 | }
82 |
83 | .center::before {
84 | background: var(--secondary-glow);
85 | border-radius: 50%;
86 | width: 480px;
87 | height: 360px;
88 | margin-left: -400px;
89 | }
90 |
91 | .center::after {
92 | background: var(--primary-glow);
93 | width: 240px;
94 | height: 180px;
95 | z-index: -1;
96 | }
97 |
98 | .center::before,
99 | .center::after {
100 | content: '';
101 | left: 50%;
102 | position: absolute;
103 | filter: blur(45px);
104 | transform: translateZ(0);
105 | }
106 |
107 | .logo {
108 | position: relative;
109 | }
110 | /* Enable hover only on non-touch devices */
111 | @media (hover: hover) and (pointer: fine) {
112 | .card:hover {
113 | background: rgba(var(--card-rgb), 0.1);
114 | border: 1px solid rgba(var(--card-border-rgb), 0.15);
115 | }
116 |
117 | .card:hover span {
118 | transform: translateX(4px);
119 | }
120 | }
121 |
122 | @media (prefers-reduced-motion) {
123 | .card:hover span {
124 | transform: none;
125 | }
126 | }
127 |
128 | /* Mobile */
129 | @media (max-width: 700px) {
130 | .content {
131 | padding: 4rem;
132 | }
133 |
134 | .grid {
135 | grid-template-columns: 1fr;
136 | margin-bottom: 120px;
137 | max-width: 320px;
138 | text-align: center;
139 | }
140 |
141 | .card {
142 | padding: 1rem 2.5rem;
143 | }
144 |
145 | .card h2 {
146 | margin-bottom: 0.5rem;
147 | }
148 |
149 | .center {
150 | padding: 8rem 0 6rem;
151 | }
152 |
153 | .center::before {
154 | transform: none;
155 | height: 300px;
156 | }
157 |
158 | .description {
159 | font-size: 0.8rem;
160 | }
161 |
162 | .description a {
163 | padding: 1rem;
164 | }
165 |
166 | .description p,
167 | .description div {
168 | display: flex;
169 | justify-content: center;
170 | position: fixed;
171 | width: 100%;
172 | }
173 |
174 | .description p {
175 | align-items: center;
176 | inset: 0 0 auto;
177 | padding: 2rem 1rem 1.4rem;
178 | border-radius: 0;
179 | border: none;
180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
181 | background: linear-gradient(
182 | to bottom,
183 | rgba(var(--background-start-rgb), 1),
184 | rgba(var(--callout-rgb), 0.5)
185 | );
186 | background-clip: padding-box;
187 | backdrop-filter: blur(24px);
188 | }
189 |
190 | .description div {
191 | align-items: flex-end;
192 | pointer-events: none;
193 | inset: auto 0 0;
194 | padding: 2rem;
195 | height: 200px;
196 | background: linear-gradient(
197 | to bottom,
198 | transparent 0%,
199 | rgb(var(--background-end-rgb)) 40%
200 | );
201 | z-index: 1;
202 | }
203 | }
204 |
205 | /* Tablet and Smaller Desktop */
206 | @media (min-width: 701px) and (max-width: 1120px) {
207 | .grid {
208 | grid-template-columns: repeat(2, 50%);
209 | }
210 | }
211 |
212 | @media (prefers-color-scheme: dark) {
213 | .vercelLogo {
214 | filter: invert(1);
215 | }
216 |
217 | .logo {
218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
219 | }
220 | }
221 |
222 | @keyframes rotate {
223 | from {
224 | transform: rotate(360deg);
225 | }
226 | to {
227 | transform: rotate(0deg);
228 | }
229 | }
230 |
--------------------------------------------------------------------------------