More progress

This commit is contained in:
Turingon 2024-08-04 17:34:29 +02:00
parent 7e2e58f7e6
commit 409bc2c3cc
26 changed files with 1459 additions and 146 deletions

View file

@ -1,4 +1,11 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
const nextConfig = {
images: {
domains:
["lh3.googleusercontent.com"]
}
}
module.exports = nextConfig
// 3:21:40

930
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -9,15 +9,17 @@
"lint": "next lint"
},
"dependencies": {
"@auth/prisma-adapter": "^2.4.1",
"@auth/prisma-adapter": "^2.4.2",
"@prisma/client": "^5.17.0",
"eslint": "8.48.0",
"eslint-config-next": "^14.2.5",
"firebase": "^10.12.5",
"next": "^14.2.5",
"next-auth": "^4.24.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-quill": "^2.0.0"
"react-quill": "^2.0.0",
"swr": "^2.2.5"
},
"devDependencies": {
"prisma": "^5.17.0"

View file

@ -1,5 +1,3 @@
generator client {
provider = "prisma-client-js"
}
@ -8,25 +6,10 @@ datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model User {
id String @id @default(auto) @map("_id") @db.ObjectId
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
// Optional for WebAuthn support
Authenticator Authenticator[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Account {
id String @id @default(cuid()) @map("_id")
userId String
userId String
type String
provider String
providerAccountId String
@ -37,31 +20,69 @@ model Account {
scope String?
id_token String?
session_state String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid()) @map("_id")
sessionToken String @unique
userId String
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model User {
id String @id @default(cuid()) @map("_id")
name String?
email String @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
Post Post[]
Comment Comment[]
}
model VerificationToken {
id String @id @map("_id")
identifier String
token String
identifier String @id @map("_id")
token String @unique
expires DateTime
@@unique([identifier, token])
}
model Category {
id String @id @default(cuid()) @map("_id")
slug String @unique
title String
img String?
Posts Post[]
}
model Post {
id String @id @default(cuid()) @map("_id")
createdAt DateTime @default(now())
slug String @unique
title String
desc String
img String?
views Int @default(0)
catSlug String
cat Category @relation(fields: [catSlug], references: [slug])
userEmail String
user User @relation(fields: [userEmail], references: [email])
comments Comment[]
}
model Comment {
id String @id @default(cuid()) @map("_id")
createdAt DateTime @default(now())
desc String
userEmail String
user User @relation(fields: [userEmail], references: [email])
postSlug String
post Post @relation(fields: [postSlug], references: [slug])
}

View file

@ -8,7 +8,7 @@ const SinglePage = () => {
<div className={styles.container}>
<div className={styles.infoContainer}>
<div className={styles.textContainer}>
<h1>Lorem ipsum dolor sit amet consectetur adipiscing elit.</h1>
<h1 className={styles.title}>Lorem ipsum dolor sit amet consectetur adipiscing elit.</h1>
<div className={styles.user}>
<div className={styles.userImageContainer}>
<Image src="/p1.jpeg" fill className={styles.image}/>

View file

@ -1,5 +1,5 @@
import { authOptions } from "@/utils/auth"
import NextAuth from "next-auth"
import { authOptions } from "@/utils/auth";
import NextAuth from "next-auth";
const handler = NextAuth(authOptions);

View file

@ -0,0 +1,15 @@
import prisma from "@/utils/connect";
import { NextResponse } from "next/server";
export const GET = async ()=>{
try{
const categories = await prisma.category.findMany();
return new NextResponse(JSON.stringify(categories, {status:200}));
} catch(err) {
console.log(err);
return new NextResponse(JSON.stringify({message: "Something went wrong!"}, {status:500}));
}
};

View file

@ -0,0 +1,51 @@
import { getAuthSession } from "@/utils/auth";
import prisma from "@/utils/connect";
import { NextResponse } from "next/server";
// GET ALL COMMENTS OF A POST
export const GET = async (req) => {
const { searchParams } = new URL(req.url);
const postSlug = searchParams.get("postSlug");
try {
const comments = await prisma.comment.findMany({
where: {
...(postSlug && { postSlug }),
},
include: { user: true },
});
return new NextResponse(JSON.stringify(comments, { status: 200 }));
} catch (err) {
// console.log(err);
return new NextResponse(
JSON.stringify({ message: "Something went wrong!" }, { status: 500 })
);
}
};
// CREATE A COMMENT
export const POST = async (req) => {
const session = await getAuthSession();
if (!session) {
return new NextResponse(
JSON.stringify({ message: "Not Authenticated!" }, { status: 401 })
);
}
try {
const body = await req.json();
const comment = await prisma.comment.create({
data: { ...body, userEmail: session.user.email },
});
return new NextResponse(JSON.stringify(comment, { status: 200 }));
} catch (err) {
console.log(err);
return new NextResponse(
JSON.stringify({ message: "Something went wrong!" }, { status: 500 })
);
}
};

View file

@ -0,0 +1,22 @@
import prisma from "@/utils/connect";
import { NextResponse } from "next/server";
// GET SINGLE POST
export const GET = async (req, {params})=>{
const {slug} = params
try{
const post = await prisma.post.findUnique({
where: {slug},
include: {user: true},
});
return new NextResponse(JSON.stringify(post, {status: 200}));
} catch(err) {
console.log(err);
return new NextResponse(JSON.stringify({message: "Something went wrong!"}, {status:500}));
}
};

View file

@ -0,0 +1,34 @@
import prisma from "@/utils/connect";
import { NextResponse } from "next/server";
export const GET = async (req)=>{
const {searchParams} = new URL(req.url);
const page = searchParams.get("page");
const cat = searchParams.get("cat");
const POST_PER_PAGE = 2;
const query = {
take: POST_PER_PAGE,
skip: POST_PER_PAGE * (page - 1),
where: {
...(cat && {catSlug: cat}),
},
};
try{
const [posts,count] = await prisma.$transaction(
[
prisma.post.findMany(query),
prisma.post.count({where:query.where}),
]
);
return new NextResponse(JSON.stringify({posts, count}, {status:200}));
} catch(err) {
console.log(err);
return new NextResponse(JSON.stringify({message: "Something went wrong!"}, {status:500}));
}
};

View file

@ -1,12 +1,9 @@
.container {
}
.title{
background-color: coral;
color: white;
padding: 5px 10px;
text-align: center;
text-transform: capitalize;
}
.content{

View file

@ -3,12 +3,15 @@ import styles from "./blogPage.module.css";
import CardList from '@/components/cardList/CardList';
import Menu from '@/components/Menu/Menu';
const BlogPage = () => {
const BlogPage = ({searchParams}) => {
const page = parseInt(searchParams.page) || 1;
const { cat } = searchParams;
return (
<div className={styles.container}>
<h1 className={styles.title}>Style Blog</h1>
<h1 className={styles.title}>{cat} Blog</h1>
<div className={styles.content}>
<CardList/>
<CardList page={page} cat={cat}/>
<Menu/>
</div>
</div>

View file

@ -8,12 +8,13 @@ const LoginPage = () => {
const {data, status} = useSession();
const router = useRouter();
console.log(data,status);
if (status === "loading") {
return <div className={styles.loading}>Loading...</div>
}
if (status === "authenticated") {
router.push("/");
return null;
}
return (
<div className={styles.container}>

View file

@ -5,13 +5,16 @@ import CardList from "@/components/cardList/CardList";
import CategoryList from "@/components/categoryList/CategoryList";
import Menu from "@/components/Menu/Menu";
export default function Home() {
export default function Home({searchParams}) {
const page = parseInt(searchParams.page) || 1;
return (
<div className={styles.container}>
<Featured/>
<CategoryList/>
<div className={styles.content}>
<CardList/>
<CardList page={page}/>
<Menu/>
</div>
</div>

View file

@ -0,0 +1,61 @@
import styles from "./singlePage.module.css";
import Menu from "@/components/Menu/Menu";
import Image from "next/image";
import Comments from "@/components/comments/Comments";
const getData = async (slug) => {
const res = await fetch(
`http://localhost:3000/api/posts/${slug}`,
{
cache: "no-store",
}
);
if (!res.ok) {
throw new Error("Failed");
}
return res.json();
};
const SinglePage = async ({params}) => {
const {slug} = params;
const data = await getData(slug);
return (
<div className={styles.container}>
<div className={styles.infoContainer}>
<div className={styles.textContainer}>
<h1 className={styles.title}>
{data?.title}
</h1>
<div className={styles.user}>
{data?.user?.image && <div className={styles.userImageContainer}>
<Image src={data.user.image} fill className={styles.image}/>
</div>}
<div className={styles.userTextContainer}>
<span className={styles.username}>{data?.user.name}</span>
<span className={styles.date}>2024.07.19</span>
</div>
</div>
</div>
{ data?.img && <div className={styles.imageContainer}>
<Image src={data.img} fill className={styles.image}/>
</div>}
</div>
<div className={styles.content}>
<div className={styles.post}>
<div className={styles.description}
dangerouslySetInnerHTML={{ __html:data?.desc }}/>
<div className={styles.comment}>
<Comments postSlug={slug}/>
</div>
</div>
<Menu/>
</div>
</div>
)
}
export default SinglePage;

View file

@ -0,0 +1,101 @@
.container{
}
.infoContainer{
display: flex;
align-items: center;
gap: 50px;
}
.textContainer{
flex: 1;
}
.title{
font-size: 64px;
margin-bottom: 50px;
}
.user{
display: flex;
align-items: center;
gap: 20px;
}
.userImageContainer{
width: 50px;
height: 50px;
position: relative;
}
.avatar{
border-radius: 50%;
object-fit: cover;
}
.userTextContainer {
display: flex;
flex-direction: column;
gap: 5px;
color: var(--softTextColor);
}
.username {
font-size: 20px;
font-weight: 500;
}
.imageContainer{
flex: 1;
height: 350px;
position: relative;
}
.image{
object-fit: cover;
}
.content {
display: flex;
gap: 50px;
}
.post {
flex: 5;
margin-top: 60px;
}
.description p{
font-size: 20px;
font-weight: 300;
margin-bottom: 20px;
}
@media screen and (max-width: 1536px) {
.title {
font-size: 54px;
}
}
@media screen and (max-width: 1280px) {
.title {
font-size: 48px;
}
}
@media screen and (max-width: 1024px) {
.imageContainer {
display: none;
}
}
@media screen and (max-width: 640px) {
.title {
font-size: 36px;
}
.description p{
font-size: 18px;
}
}

View file

@ -5,12 +5,24 @@ import Image from "next/image";
import { useState } from "react";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.bubble.css";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
const WritePage = () => {
const [open, setOpen] = useState(false)
const {status} = useSession();
const router = useRouter();
const [file, setFile] = useState(null);
const [open, setOpen] = useState(false);
const [value, setValue] = useState("");
if (status === "loading") {
return <div className={styles.loading}>Loading...</div>
}
if (status === "authenticated") {
router.push("/");
}
return (
<div className={styles.container}>
<input type="text" placeholder="Title" className={styles.input}/>
@ -20,9 +32,19 @@ const WritePage = () => {
</button>
{open && (
<div className={styles.add}>
<input
type="file"
id="image"
onChange={e=>setFile(e.target.files[0])}
style={ { display: "none" }}
/>
<button className={styles.addButton}>
<Image src="/image.png" alt="" width={16} height={16} />
<label htmlFor="image">
<Image src="/image.png" alt="" width={16} height={16} />
</label>
</button>
<button className={styles.addButton}>
<Image src="/external.png" alt="" width={16} height={16} />

View file

@ -28,7 +28,7 @@ const AuthLinks = () => {
<Link href="/">Homepage</Link>
<Link href="/">About</Link>
<Link href="/">Contact</Link>
{status==="notauthenticated" ? (
{status==="unauthenticated" ? (
<Link href="/login">Login</Link>
) : (
<>

View file

@ -2,28 +2,29 @@ import styles from "./card.module.css";
import Image from "next/image";
import Link from "next/link";
const Card = () => {
const Card = ({key, item}) => {
return (
<div className={styles.container}>
<div className={styles.container} key={key}>
{item.img &&
<div className={styles.imageContainer}>
<Image src="/p1.jpeg" alt="" fill className={styles.image}/>
</div>
<Image src={item.img} alt="" fill className={styles.image}/>
</div>}
<div className={styles.textContainer}>
<div className={styles.detail}>
<span className={styles.date}>
2023.02.11 -&nbsp;
{item.createdAt.substring(0,10)} -{" "}
</span>
<span className={styles.category}>
CULTURE
{item.catSlug}
</span>
</div>
<Link href="/">
<h1>Lorem ipsum dolor sit amet consectetur adipiscing elit.</h1>
<Link href={`/post/${item.slug}`}>
<h1>{item.title}</h1>
</Link>
<p className={styles.desc}>
Some more Lorem Ipsum xD.
{item.desc.substring(0,60)}
</p>
<Link href="/" className={styles.link}>
<Link href={`/post/${item.slug}`} className={styles.link}>
Read More
</Link>
</div>

View file

@ -1,22 +1,43 @@
import React from 'react';
import React from "react";
import styles from "./cardlist.module.css";
import Pagination from '../pagination/Pagination';
import Pagination from "../pagination/Pagination";
import Image from "next/image";
import Card from '../card/Card';
import Card from "../card/Card";
const CardList = () => {
return (
<div className={styles.container}>
<h1>Recent Posts</h1>
<div className={styles.posts}>
<Card />
<Card />
<Card />
<Card />
</div>
<Pagination/>
</div>
)
}
const getData = async (page, cat) => {
const res = await fetch(
`http://localhost:3000/api/posts?page=${page}&cat=${cat || ""}`,
{
cache: "no-store",
}
);
if (!res.ok) {
throw new Error("Failed");
}
return res.json();
};
const CardList = async ({ page, cat }) => {
const { posts, count } = await getData(page, cat);
const POST_PER_PAGE = 2;
const hasPrev = POST_PER_PAGE * (page - 1) > 0;
const hasNext = POST_PER_PAGE * (page - 1) + POST_PER_PAGE < count;
return (
<div className={styles.container}>
<h1 className={styles.title}>Recent Posts</h1>
<div className={styles.posts}>
{posts?.map((item) => (
<Card item={item} key={item._id} />
))}
</div>
<Pagination page={page} hasPrev={hasPrev} hasNext={hasNext} />
</div>
);
};
export default CardList;

View file

@ -3,40 +3,32 @@ import styles from "./categoryList.module.css";
import Link from "next/link";
import Image from "next/image";
const CategoryList = () => {
const getData = async ()=> {
const res = await fetch("http://localhost:3000/api/categories", {cache: "no-store"})
if (!res.ok) {
throw new Error("Failed")
}
return res.json()
}
const CategoryList = async () => {
const data = await getData();
return (
<div className={styles.container}>
<h1 className={styles.title}>Categories</h1>
<div className={styles.categories}>
<Link href="/blog?cat=style" className={`${styles.category} ${styles.style}`}>
<Image src="/style.png" alt="" width={32} height={32} className={styles.image} />
style
</Link>
{data?.map(item=>(
<Link href={`/blog`} className={`${styles.category} ${styles.fashion}`}>
<Image src="/fashion.png" alt="" width={32} height={32} className={styles.image} />
philosophy
<Link href="/blog?cat=style" className={`${styles.category} ${styles[item.slug]}`} key={item._id}>
{item.img && (<Image src={item.img} alt="" width={32} height={32} className={styles.image} />)}
{item.title}
</Link>
))}
<Link href={`/blog`} className={`${styles.category} ${styles.food}`}>
<Image src="/food.png" alt="" width={32} height={32} className={styles.image} />
food
</Link>
<Link href={`/blog`} className={`${styles.category} ${styles.travel}`}>
<Image src="/travel.png" alt="" width={32} height={32} className={styles.image} />
travel
</Link>
<Link href={`/blog`} className={`${styles.category} ${styles.culture}`}>
<Image src="/culture.png" alt="" width={32} height={32} className={styles.image} />
culture
</Link>
<Link href={`/blog`} className={`${styles.category} ${styles.coding}`}>
<Image src="/coding.png" alt="" width={32} height={32} className={styles.image} />
coding
</Link>
</div>
</div>

View file

@ -1,34 +1,64 @@
"use client"
import styles from "./comments.module.css"
import Link from "next/link";
import Image from "next/image";
import useSWR from "swr";
import { useSession } from "next-auth/react";
import { useState } from "react";
const Comments = () => {
const status = "authenticated";
const fetcher = async (url) => {
const res = await fetch(url);
const data = await res.json();
if (!res.ok) {
const error = new Error(data.message);
throw error;
}
return data;
};
const Comments = ({postSlug}) => {
const {status} = useSession();
const {data, mutate, isLoading} = useSWR(`http://localhost:3000/api/comments?postSlug=${postSlug}`, fetcher);
const [desc, setDesc] = useState("");
const handleSubmit = async () => {
await fetch("/api/comments", {
method: "POST",
body: JSON.stringify({desc, postSlug}),
})
mutate();
}
console.log("Fetched data:", data);
console.log("Session status:", status);
return (
<div className={styles.container}>
<h1 className={styles.title}>Comments</h1>
{status === "authenticated" ? (
<div className={styles.write}>
<textarea placeholder="write a comment..." className={styles.input}/>
<button className={styles.button}>Send</button>
<textarea placeholder="write a comment..." className={styles.input} onChange={e=>setDesc(e.target.value)}/>
<button className={styles.button} onClick={handleSubmit}>Send</button>
</div>
) : (
<Link href="/login">Login to write a comment</Link>)}
<div className={styles.comments}>
<div className={styles.comment}>
<div className={styles.user}>
<Image src="/p1.jpeg" alt="" width={50} height={50} className={styles.image}/>
<div className={styles.userInfo}>
<span className={styles.username}>Max Mustermann</span>
<span className={styles.date}>2024.02.01</span>
{isLoading ? "loading" : Array.isArray(data) ? data.map((item)=> (
<div className={styles.comment} key={item._id}>
<div className={styles.user}>
{item?.user?.image && (<Image src={item.user.image} alt="" width={50} height={50} className={styles.image}/>)}
<div className={styles.userInfo}>
<span className={styles.username}>{item.user.name}</span>
<span className={styles.date}>{item.createdAt}</span>
</div>
</div>
<p className={styles.desc}>{item.desc}</p>
</div>
<p className={styles.desc}>Lorem ipsum oder etwas in die Richtung</p>
</div>
)) : null}
</div>
</div>
)
);
}
export default Comments;

View file

@ -1,11 +1,23 @@
"use client";
import React from 'react';
import styles from "./pagination.module.css";
import { useRouter } from 'next/navigation';
const Pagination = ({page, hasPrev, hasNext}) => {
const router = useRouter();
const Pagination = () => {
return (
<div className={styles.container}>
<button className={styles.button}>Malantaŭen</button>
<button className={styles.button}>Antaŭen</button>
<button className={styles.button}
disabled={!hasPrev}
onClick={()=>router.push(`?page=${page-1}`)}>
Malantaŭen
</button>
<button className={styles.button}
disabled={!hasNext}
onClick={()=>router.push(`?page=${page+1}`)}>
Antaŭen
</button>
</div>
)
}

View file

@ -10,4 +10,9 @@
background-color: crimson;
color: white;
cursor: pointer;
}
.button:disabled{
background-color: rgba(220, 20, 60, 0.473);
cursor: not-allowed;
}

View file

@ -6,7 +6,7 @@ import { useContext } from "react";
import { ThemeContext } from "@/context/ThemeContext";
const ThemeToggle = () => {
const {toggle, theme} = useContext(ThemeContext)
const {toggle, theme} = useContext(ThemeContext);
return <div className={styles.container} onClick={toggle} style={
theme === "dark"

View file

@ -1,21 +1,21 @@
import { PrismaAdapter } from "@auth/prisma-adapter"
import NextAuth from "next-auth/next"
import GithubProvider from "next-auth/providers/github"
import GoogleProvider from "next-auth/providers/google"
import prisma from "./connect"
import { PrismaAdapter } from "@auth/prisma-adapter";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import prisma from "./connect";
import { getServerSession } from "next-auth";
export const authOptions = {
adapters: PrismaAdapter(prisma),
// Configure one or more authentication providers
adapter: PrismaAdapter(prisma),
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
// ...add more providers here
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
}
};
export const getAuthSession = () => getServerSession(authOptions);