alot and reverse proxy
This commit is contained in:
+8
-1
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'{' '}
|
||||
I have read and agree to Tweakers'{' '}
|
||||
<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');
|
||||
});
|
||||
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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
.trending{
|
||||
width: 60%;
|
||||
margin: 0 auto;
|
||||
a {
|
||||
@include buttons.button;
|
||||
float: right;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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
@@ -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: `"${quotes[currentQuoteIndex].text}"` }}></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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export const metadata: Metadata = {
|
||||
openGraph: {
|
||||
...openGraphMetadata,
|
||||
title: 'Create an account',
|
||||
url: 'https://quiztimes.nl/register',
|
||||
url: 'https://Forum.nl/register',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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%),
|
||||
|
||||
Reference in New Issue
Block a user