alot and reverse proxy

This commit is contained in:
2024-03-11 18:12:31 +01:00
parent 751d57d58b
commit 914fc91731
19 changed files with 1209 additions and 41 deletions
+8 -1
View File
@@ -2,11 +2,18 @@ const path = require('path');
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
distDir: 'out',
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
},
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'http://localhost:5000/:path*', // Proxy to Backend IMPORTANT TO SET THE CORRECT PORT
},
];
}
};
module.exports = nextConfig;
+4 -1
View File
@@ -8,7 +8,10 @@ import { setSession } from '../_helpers/auth';
export default function Sessiondata() {
const pathname: string = usePathname().substring(1);
var username: string = localStorage.getItem('username') || '';
var username: string = '';
// if(typeof window !== 'undefined'){
username = localStorage.getItem('username') || '';
// }
if (pathname === 'register' || pathname === 'login') {
return null;
+48 -9
View File
@@ -54,7 +54,7 @@ export function RegisterForm() {
<input id="password" type="password" minLength={5} required></input>
<input id="consent" type="checkbox" required />
<label htmlFor="consent">
I have read and agree to Quiztimes&apos;{' '}
I have read and agree to Tweakers&apos;{' '}
<Link href={'/terms'}>Terms of Service</Link>
</label>
<button type="submit" className={inter.className}>
@@ -72,7 +72,7 @@ function handleLogin(redirUrl: string | null) {
.value,
};
fetch('https://quiztimes.nl/api/login', {
fetch('/api/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -111,7 +111,7 @@ async function handleRegister(redirUrl: string | null) {
.value,
};
fetch('https://quiztimes.nl/api/register', {
fetch('/api/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -142,16 +142,55 @@ async function handleRegister(redirUrl: string | null) {
}
export async function setSession(setLoggedIn: boolean) {
console.log('test');
if (setLoggedIn) {
await fetch('https://quiztimes.nl/api/jwt').then((data) => {
console.log(data);
localStorage.setItem('username', 'Vincent');
});
return true;
try {
const response = await fetch('/api/jwt');
if (!response.ok) {
throw new Error('Failed to fetch JWT');
}
const data = await response.json();
localStorage.setItem('username', data.user);
return true;
} catch (error) {
console.error('Error fetching JWT:', error);
return false;
}
} else {
localStorage.clear();
window.location.href = '/';
return false;
}
}
export async function logout() {
await setSession(false);
//remove cookies from server
fetch('/api/logout', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
}
});
}
export async function refreshToken() {
try {
const response = await fetch('/api/refreshtoken', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.text();
console.log(data);
if (!response.ok) {
setSession(false);
//redirect to login page
window.location.href = '/login?redir=' + window.location.pathname;
}
} catch (error) {
console.error('Error refreshing token:', error);
}
}
setInterval(refreshToken, 300000);
+42
View File
@@ -0,0 +1,42 @@
.BrowseField {
width: 1200px;
height: 60px;
border-radius: 16px;
background-color: var(--off-white);
.Info {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between; /* Pushes elements to the ends */
.title {
color: var(--brand);
padding-left: 20px;
font-size: 30px;
}
p {
color: var(--black);
font-size: 30px;
margin-right: 20px; /* Adjust the spacing between elements */
}
.headtitle{
color: var(--brand);
font-weight: 800;
font-size: 30px;
padding-left: 20px;
}
.headdescription{
font-weight: 600;
}
.headcount{
font-weight: 600;
}
}
}
+35
View File
@@ -0,0 +1,35 @@
import styles from './browseBlock.module.scss'
import Link from 'next/link'
type BrowseBlockProps = {
title: string,
description: string,
postcount: number | string,
}
export function BrowseBlock(BrowseBlockProps: BrowseBlockProps) {
return (
<div className={styles.BrowseField}>
<div className={styles.Info}>
<Link href={`/category/${BrowseBlockProps.title}`}><h2 className={styles.headtitle} >{BrowseBlockProps.title}</h2></Link>
<p className={styles.description}>{BrowseBlockProps.description}</p>
<p className={styles.count}>{BrowseBlockProps.postcount} posts</p>
</div>
</div>
)
}
export function FirsBlock(BrowseBlockProps: BrowseBlockProps) {
return (
<div className={styles.BrowseField}>
<div className={styles.Info}>
<h2 className={styles.title} >{BrowseBlockProps.title}</h2>
<p className={styles.headdescription}>{BrowseBlockProps.description}</p>
<p className={styles.headcount}>{BrowseBlockProps.postcount}</p>
</div>
</div>
)
}
+35
View File
@@ -0,0 +1,35 @@
.wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 5rem 22rem;
.header {
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 4rem;
h1 {
float: left;
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
}
.contentGrid {
display: grid;
gap: 1rem;
}
p {
font-size: 1.5rem;
font-weight: 600;
color: var(--white);
padding: 1rem;
}
}
+69
View File
@@ -0,0 +1,69 @@
'use client'
import Link from 'next/link';
import styles from './page.module.scss';
import { useState, useEffect } from 'react';
import { BrowseBlock, FirsBlock } from './browseBlock';
type Category = {
name: string;
description: string;
count: number;
};
export default function Page() {
const [categories, setCategories] = useState<Category[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
async function fetchCategories() {
try {
const response = await fetch('/api/home/categories');
if (!response.ok) {
throw new Error('Failed to fetch categories');
}
const data = await response.json();
const categorieslist = data['categories']
console.log(data['categories'].length)
setCategories(categorieslist instanceof Array ? categorieslist : []);
setLoading(false); // Set loading to false once data is fetched
} catch (error) {
console.error('Error fetching categories:', error);
}
}
fetchCategories();
}, []);
return (
<main className={styles.main}>
<div className={styles.wrapper}>
<div className={styles.header}>
<h1 className={styles.title}>Browse Categories</h1>
</div>
<div className={styles.contentGrid}>
<FirsBlock
title="Name:"
description="Description:"
postcount="Post count:"
/>
{loading ? (
<p>Loading...</p>
) : categories.length == 0 ? (
<p>No categories found</p>
) : (
categories.map(category => (
<BrowseBlock
key={category.name} // Assuming each category has a unique name
title={category.name}
description={category.description}
postcount={category.count}
/>
))
)}
</div>
</div>
</main>
);
}
@@ -0,0 +1,57 @@
.BrowseField {
width: 1200px;
height: 60px;
border-radius: 16px;
background-color: var(--off-white);
.Info {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: auto 1fr 1fr auto; /* Setting a fixed width for the title column */
align-items: center;
justify-content: space-between;
.title {
color: var(--brand);
font-size: 30px;
white-space: nowrap; /* Prevent wrapping of text */
overflow: hidden; /* Hide overflowing text */
text-overflow: ellipsis; /* Show ellipsis for overflowed text */
width: 400px; /* Limit the maximum width of the title */
}
p {
color: var(--black);
font-size: 30px;
white-space: nowrap; /* Prevent wrapping of text */
overflow: hidden; /* Hide overflowing text */
text-overflow: ellipsis; /* Show ellipsis for overflowed text */
}
.description {
max-width: 200px;
}
.created {
width: 400px
}
.headtitle {
color: var(--brand);
font-weight: 800;
font-size: 30px;
width: 400px; /* Limit the maximum width of the title */
}
.headdescription {
font-weight: 600;
width: 200px;
}
.headcount {
font-weight: 600;
padding-right: 50px;
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import styles from './browseBlock.module.scss'
import Link from 'next/link'
type BrowseBlockProps = {
title: string,
author: string,
reactions: number | string,
created: string,
id: string;
}
export function BrowseBlock(BrowseBlockProps: BrowseBlockProps) {
return (
<div className={styles.BrowseField}>
<div className={styles.Info}>
<Link href={`/post/${BrowseBlockProps.id}`}><h2 className={styles.title} >{BrowseBlockProps.title}</h2></Link>
<p className={styles.description}>{BrowseBlockProps.author}</p>
<p className={styles.created}>{BrowseBlockProps.created}</p>
<p className={styles.count}>{BrowseBlockProps.reactions} 5 reactions</p>
</div>
</div>
)
}
export function FirsBlock(BrowseBlockProps: BrowseBlockProps) {
return (
<div className={styles.BrowseField}>
<div className={styles.Info}>
<h2 className={styles.headtitle} >{BrowseBlockProps.title}</h2>
<p className={styles.headdescription}>{BrowseBlockProps.author}</p>
<p className={styles.headcount}>{BrowseBlockProps.created}</p>
<p className={styles.headcount}>{BrowseBlockProps.reactions}</p>
</div>
</div>
)
}
+46
View File
@@ -0,0 +1,46 @@
.wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 5rem 22rem;
.header {
display: flex;
width: 100%;
flex-direction: row;
justify-content: space-between;
align-items: center;
margin-bottom: 4rem;
h1 {
float: left;
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
}
.contentGrid{
display: grid;
gap: 1rem;
}
.loadMore {
display: flex;
justify-content: center;
align-items: center;
margin: 4rem 0 2rem;
background-color: var(--brand);
border-radius: 8px;
border: none;
p {
font-size: 1.5rem;
font-weight: 600;
color: var(--white);
padding: 1rem;
}
}
}
+98
View File
@@ -0,0 +1,98 @@
'use client'
import Link from 'next/link';
import styles from './page.module.scss';
import { useState, useEffect } from 'react';
import { BrowseBlock, FirsBlock } from './browseBlock';
type Post = {
title: string;
author: string;
reactions: number;
date: string;
id: string;
};
export default function Page({ params }: { params: { slug: string } }) {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [categoryDescription, setCategoryDescription] = useState<string>('');
useEffect(() => {
async function fetchPostsCategory() {
try {
const response = await fetch('/api/home/category/' + params.slug);
if (!response.ok) {
throw new Error('Failed to fetch categories');
}
const data = await response.json();
const categorieslist = data['postarray']
console.log(data['postarray'].length)
setPosts(categorieslist instanceof Array ? categorieslist : []);
setLoading(false); // Set loading to false once data is fetched
} catch (error) {
console.error('Error fetching categories:', error);
}
}
async function fetchCategoryDescription() {
try {
const response = await fetch('/api/home/category/details/' + params.slug);
if (!response.ok) {
throw new Error('Failed to fetch categories');
}
const data = await response.json();
setCategoryDescription(data['description']);
} catch (error) {
console.error('Error fetching categories:', error);
}
}
fetchCategoryDescription();
fetchPostsCategory();
}, []);
//fetch description of category on /api/home/category/details/:slug
return (
<main className={styles.main}>
<div className={styles.wrapper}>
<div className={styles.header}>
<h1 className={styles.title}>{params.slug}</h1>
<p>{categoryDescription}</p>
</div>
<div className={styles.contentGrid}>
<FirsBlock
title="Name:"
author="Creator:"
reactions="Reactions:"
created="Created:"
id="id"
/>
{loading ? (
<p>Loading...</p>
) : posts.length == 0 ? (
<p>No categories found</p>
) : (
posts.map(Post => (
<BrowseBlock
key={Post.title} // Assuming each category has a unique name
title={Post.title}
author={Post.author}
reactions={Post.reactions}
created={Post.date}
id={Post.id}
/>
))
)}
</div>
</div>
</main>
);
}
+242 -6
View File
@@ -1,11 +1,247 @@
@use '@/styles/buttons';
.main {
padding-top: 5rem;
padding-left: 5rem;
.banner {
margin-top: -50px;
height: 100vh;
padding: 0px 32px;
background: linear-gradient(45deg,
var(--almost-black) 0%,
var(--dark-gray) 100%);
.content {
position: relative;
top: 50%;
transform: translateY(-50%);
margin: auto;
max-width: 1400px;
h1 {
margin-bottom: 32px;
font-size: 2.5rem;
font-weight: bold;
text-align: right;
color: var(--white);
span {
color: var(--brand);
}
}
a {
@include buttons.button;
float: right;
font-size: 1.5rem;
}
}
}
.trending{
width: 60%;
margin: 0 auto;
.demo {
padding: 128px 32px;
background-color: var(--off-white);
color: var(--almost-black);
span {
color: var(--brand);
}
.content {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 64px;
margin: auto;
max-width: 1400px;
.action {
margin-top: 32px;
min-width: 384px;
h2 {
margin-bottom: 32px;
font-size: 2.5rem;
}
a {
@include buttons.button;
font-size: 1.2rem;
}
p {
margin-top: 16px;
font-size: 0.8rem;
}
}
.quoteContainer {
float: right;
border: 2px solid var(--almost-black);
border-radius: 8px;
box-sizing: border-box;
padding: 32px;
@media screen and (min-width: 600px) {
min-width: 512px;
}
h3 {
margin-bottom: 24px;
font-size: 2.5rem;
}
}
}
}
.flashcards {
padding: 256px 32px;
background-color: var(--white);
color: var(--black);
}
.customise {
position: relative;
padding: 128px 32px;
background-color: var(--almost-black);
color: var(--white);
.content {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 64px;
margin: auto;
max-width: 1400px;
.header {
width: fit-content;
h2 {
font-size: 4rem;
text-align: center;
}
.topics {
margin: 0 auto;
margin-top: 32px;
width: 600px;
height: 128px;
.choice {
position: relative;
display: grid;
grid-template-columns: repeat(2, 1fr);
justify-content: center;
gap: 16px;
height: 100%;
div {
border: 2px solid var(--white);
border-radius: 8px;
box-sizing: border-box;
cursor: pointer;
p {
position: relative;
top: 50%;
transform: translateY(-50%);
font-weight: bold;
text-align: center;
}
}
}
.open {
position: absolute;
top: 50%;
transform: translateY(-50%);
width: 600px;
input {
display: block;
border: 2px solid var(--white);
border-radius: 8px;
box-sizing: border-box;
outline: none;
width: 100%;
height: 50px;
padding: 8px;
background-color: transparent;
color: var(--white);
}
}
}
.toggle {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 32px;
p {
width: 128px;
line-height: 24px;
font-weight: bold;
&:first-of-type {
text-align: right;
}
}
label {
position: relative;
border: 2px solid var(--white);
border-radius: 64px;
width: 48px;
height: 24px;
cursor: pointer;
input {
opacity: 0;
width: 0;
height: 0;
}
span {
position: absolute;
left: 4px;
top: 4px;
border-radius: 64px;
width: 16px;
height: 16px;
background-color: var(--brand);
}
// input:checked + span {
// left: 28px;
// }
}
}
}
.selector {
text-align: right;
h2 {
margin-bottom: 16px;
font-size: 2.5rem;
span {
color: var(--brand);
}
}
.themes {
display: flex;
justify-content: flex-end;
gap: 16px;
margin-top: 48px;
div {
display: inline-block;
border-radius: 8px;
width: 48px;
height: 48px;
}
}
}
}
}
+269 -23
View File
@@ -6,35 +6,281 @@ import Link from 'next/link';
import { motion } from 'framer-motion';
import { useState, useEffect } from 'react';
const PostArray = [
{
title:'problems with running docker on arm processor',
content:'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl eget aliquam ultricies, nunc nisl ultricies nunc, vitae aliquam ni du tein kanker moederus',
author:'testuser1234567',
category:'Technology',
postId:'1',
date:'01-02-2023',
reactionCount:'69',
},
{
title:'problems with running docker on arm processor',
content:'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl eget aliquam ultricies, nunc nisl ultricies nunc, vitae aliquam ni du tein kanker moederus',
author:'testuser1234567',
category:'Technology',
postId:'1',
date:'01-02-2023',
reactionCount:'69',
}
]
export default function Home() {
const [selectedTheme, setSelectedTheme] = useState('black');
const username = localStorage.getItem('username');
if (username) {
window.location.href = '/account';
}
const quotes = [
{
text: "<span>Forum</span> is a nice place to spend time.",
author: "Kaj, High School Student"
},
{
text: "<span>Forum</span> is one of the best forums.",
author: "Ruben, High School Student"
}
];
const [currentQuoteIndex, setCurrentQuoteIndex] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
// Switch to the next quote
setCurrentQuoteIndex((prevIndex) => (prevIndex + 1) % quotes.length);
}, 5000); // Change the interval as needed (in milliseconds)
return () => clearInterval(interval); // Cleanup the interval on component unmount
}, []);
return (
<main className={styles.main}>
<div className={styles.trending}>
<h2>Trending</h2>
<div className={styles.banner}>
<div className={styles.content}>
<motion.h1
initial={{ opacity: 0, y: '40px' }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
Ask an answer question <br />
with <span>Forum</span>
</motion.h1>
<motion.p
initial={{ opacity: 0, y: '40px' }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
<Link href={'/register'}>Start today</Link>
</motion.p>
</div>
</div>
<div className={styles.demo}>
<div className={styles.content}>
<motion.div
className={styles.action}
variants={{
visible: {
transition: {
delayChildren: 0.2,
staggerChildren: 0.5,
},
},
}}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.h2
variants={{
hidden: { opacity: 0, x: '-40px' },
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.5 },
},
}}
>
Talk about your
<br />
favorite topics
</motion.h2>
<motion.div
variants={{
hidden: { opacity: 0 },
visible: { opacity: 1 },
}}
>
<Link href={'/register'}>Start now</Link>
<p>*An account is required.</p>
</motion.div>
</motion.div>
<div className={styles.quoteContainer}>
<h3 dangerouslySetInnerHTML={{ __html: `&quot;${quotes[currentQuoteIndex].text}&quot;` }}></h3>
<p>{quotes[currentQuoteIndex]["author"]}</p>
</div>
</div>
</div>
{/* <div className={styles.flashcards}>
<p></p>
</div> */}
<motion.div
className={styles.customise}
variants={{
black: {
backgroundColor: 'var(--almost-black)',
color: 'var(--white)',
},
white: {
backgroundColor: 'var(--off-white)',
color: 'var(--black)',
},
brand: {
backgroundColor: 'var(--brand)',
color: 'var(--white)',
},
cozy: {
backgroundColor: 'var(--cozy)',
color: 'var(--almost-black)',
},
}}
animate={selectedTheme}
>
<div className={styles.content}>
<div className={styles.header}>
<h2>Hot topics</h2>
<div className={styles.topics}>
<div className={styles.choice}>
<motion.div
variants={{
black: {
borderColor: 'var(--white)',
},
white: {
borderColor: 'var(--black)',
},
brand: {
borderColor: 'var(--white)',
},
cozy: {
borderColor: 'var(--almost-black)',
},
}}
onClick={() => {
window.location.href = '/topic/2';
}}
>
<p>Topic 1</p> {/* TODO: make it update with the latest topic */}
</motion.div>
<motion.div
variants={{
black: {
borderColor: 'var(--white)',
},
white: {
borderColor: 'var(--black)',
},
brand: {
borderColor: 'var(--white)',
},
cozy: {
borderColor: 'var(--almost-black)',
},
}}
//onclick go to other page
onClick={() => {
window.location.href = '/topic/2';
}}
>
<p>Topic 2</p> {/* TODO: make it update with the latest topic */}
</motion.div>
</div>
</div>
</div>
<div className={styles.selector}>
<h2>
Customise{' '}
<motion.span
variants={{
black: {
color: 'var(--brand)',
},
white: {
color: 'var(--brand)',
},
brand: {
color: 'var(--dark-gray)',
},
cozy: {
color: 'var(--white)',
},
}}
>
your
</motion.span>{' '}
<br />
experience
</h2>
<p>With themes</p>
<motion.div
className={styles.themes}
variants={{
visible: {
transition: {
delayChildren: 0.2,
staggerChildren: 0.2,
},
},
}}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.div
style={{
backgroundColor: 'var(--almost-black)',
}}
variants={{
hidden: { opacity: 0, y: '10px' },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
}}
onMouseOver={() => {
setSelectedTheme('black');
}}
></motion.div>
<motion.div
style={{ backgroundColor: 'var(--off-white)' }}
variants={{
hidden: { opacity: 0, y: '10px' },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
}}
onMouseOver={() => {
setSelectedTheme('white');
}}
></motion.div>
<motion.div
style={{ backgroundColor: 'var(--brand)' }}
variants={{
hidden: { opacity: 0, y: '10px' },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
}}
onMouseOver={() => {
setSelectedTheme('brand');
}}
></motion.div>
<motion.div
style={{ backgroundColor: 'var(--cozy)' }}
variants={{
hidden: { opacity: 0, y: '10px' },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5 },
},
}}
onMouseOver={() => {
setSelectedTheme('cozy');
}}
></motion.div>
</motion.div>
</div>
</div>
</motion.div>
</main>
);
}
View File
+103
View File
@@ -0,0 +1,103 @@
.main{
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.wrapper{
display: flex;
flex-direction: column;
background-color: var(--off-white);
border-radius: 16px;
.loading{
font-size: 10rem;
}
.topinfo{
width: 1200px;
margin-left: 10px;
margin-top: 15px;
h1{
font-size: 3rem;
font-weight: bold;
color: var(--black);
}
.metadata{
display: flex;
align-items: center;
.author{
font-size: 1.5rem;
color: var(--brand);
font-weight: 600;
}
p{
margin-right: 15px;
}
}
p{
font-size: 1.2rem;
color: var(--black);
}
}
.content{
margin-top: 15px;
margin-left: 10px;
margin-right: 10px;
margin-bottom: 15px;
pre{
font-size: 1.4rem;
color: var(--black);
line-height: 1.5;
max-width: 1150px;
white-space: pre-wrap;
word-wrap: break-word;
overflow: auto;
}
}
.bottominfo{
margin-left: 10px;
margin-right: 10px;
margin-bottom: 15px;
display: flex;
align-items: center;
justify-content: space-between;
.button {
display: flex;
justify-content: center;
align-items: center;
margin: 4rem 0 2rem;
background-color: var(--brand);
border-radius: 8px;
border: none;
p{
font-size: 1.3rem;
color: var(--black);
}
}
.button:hover{
//chance cursor
cursor: pointer;
//change color
background-color: var(--brand-dark);
}
}
.comments{
}
}
}
+111
View File
@@ -0,0 +1,111 @@
'use client'
import Link from 'next/link';
import styles from './page.module.scss';
import { useState, useEffect } from 'react';
type Post = {
title: string;
author: string;
comments: [];
likes: number;
saves: number;
content: string;
category: string;
date: string;
viewcount: number;
};
export default function Page({ params }: { params: { slug: string } }) {
//fetch the data
const [post, setPost] = useState<Post>({ title: '', author: '', comments: [], likes: 0, saves: 0, content: '', category: '', date: '', viewcount: 0 });
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
async function fetchPostsCategory() {
try {
const response = await fetch('/api/post/' + params.slug);
if (!response.ok) {
throw new Error('Failed to fetch categories');
}
const data = await response.json();
setPost(data['post'][0]);
setLoading(false); // Set loading to false once data is fetched
} catch (error) {
console.error('Error fetching categories:', error);
}
}
fetchPostsCategory();
}, []);
async function likePost() {
try {
const response = await fetch('/api/post/like/' + params.slug, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error('Failed to like post');
}
const data = await response.json();
//set post likes to data.likes
setPost({ ...post, likes: data.likes });
} catch (error) {
console.error('Error liking post:', error);
}
}
async function savePost() {
try {
const response = await fetch('/api/post/save/' + params.slug , {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
});
if (!response.ok) {
throw new Error('Failed to save post');
}
const data = await response.json();
setPost({ ...post, saves: data.saves });
} catch (error) {
console.error('Error saving post:', error);
}
}
return (
<main className={styles.main}>
<div className={styles.wrapper}>
{loading ? (
<p className={styles.loading}>Loading...</p>
) : (
<div>
<div className={styles.topinfo}>
<h1>{post.title}</h1>
<div className={styles.metadata}>
<Link href={'/' + post.author}><p className={styles.author}>{post.author}</p></Link>
<p>created on: {post.date}</p>
<p>{post.viewcount} views</p>
</div>
</div>
<div className={styles.content}>
<pre>{post.content} to see how good it is </pre>
</div>
<div className={styles.bottominfo}>
<button onClick={likePost} className={styles.button}><p>likes: {post.likes}</p></button>
<button onClick={savePost} className={styles.button}><p>saves: {post.saves}</p></button>
</div>
<div className={styles.comments}>
</div>
</div>
)}
</div>
</main>
);
}
+1 -1
View File
@@ -12,7 +12,7 @@ export const metadata: Metadata = {
openGraph: {
...openGraphMetadata,
title: 'Create an account',
url: 'https://quiztimes.nl/register',
url: 'https://Forum.nl/register',
},
};
+2
View File
@@ -4,6 +4,8 @@
$colors: (
brand: hsl(237, 100%, 70%),
brand-dark: hsl(237, 100%, 60%),
brand-light: hsl(237, 100%, 80%),
white: hsl(0, 0%, 100%),
off-white: hsl(0, 0%, 96%),
gray: hsl(0, 0%, 50%),