From 471fad6d61312c7ebaec79538aa2bb3bcab7e33f Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Sat, 5 Nov 2022 22:51:37 +0100 Subject: [PATCH] Added some pages --- frontend/src/components/Problem.jsx | 7 +- frontend/src/hooks.jsx | 15 ++ frontend/src/main.jsx | 2 - frontend/src/pages/Home.jsx | 51 ++++-- frontend/src/pages/Login.jsx | 38 +++- frontend/src/pages/NewSolution.jsx | 61 ------- frontend/src/pages/Problem.jsx | 78 ++++++++- frontend/styles/main.scss | 260 ++++++++++++++++++++-------- frontend/vite.config.js | 2 +- main.js | 41 ++++- package.json | 1 + pnpm-lock.yaml | 15 ++ server/helpers.js | 13 ++ 13 files changed, 408 insertions(+), 176 deletions(-) create mode 100644 frontend/src/hooks.jsx delete mode 100644 frontend/src/pages/NewSolution.jsx diff --git a/frontend/src/components/Problem.jsx b/frontend/src/components/Problem.jsx index 1566c87..368a030 100644 --- a/frontend/src/components/Problem.jsx +++ b/frontend/src/components/Problem.jsx @@ -1,11 +1,14 @@ +import { Link } from '../Router.jsx' import { Markdown } from './Markdown.jsx' -export const Problem = ({ title, content }) => { +export const Problem = ({ id, content }) => { return (
- + + Problema {id} +
diff --git a/frontend/src/hooks.jsx b/frontend/src/hooks.jsx new file mode 100644 index 0000000..3e941af --- /dev/null +++ b/frontend/src/hooks.jsx @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'preact/hooks' + +export const useUser = () => { + const [username, setUsername] = useState(null) + + useEffect(async () => { + const res = await fetch(`/api/current-user`, { + credentials: 'include', + }) + const username = await res.json() + setUsername(username) + }, []) + + return { username } +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 714a8af..01573d1 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -5,7 +5,6 @@ import { Router } from './Router.jsx' import { HomePage } from './pages/Home.jsx' import { LoginPage } from './pages/Login.jsx' import { ProblemPage } from './pages/Problem.jsx' -import { NewSolutionPage } from './pages/NewSolution.jsx' render( , document.body diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 64d3924..ffe2f6d 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,26 +1,45 @@ -import { computed, useSignal } from '@preact/signals' -import { Markdown } from '../components/Markdown.jsx' +import { Problem } from '../components/Problem.jsx' +import { useUser } from '../hooks.jsx' import { Link } from '../Router.jsx' export const HomePage = () => { + const { username } = useUser() + + const logout = async () => { + await fetch(`/api/logout`, { + method: 'POST', + }) + + location.reload() + } + + const problems = Array.from({ length: 20 }, (_, i) => ({ + id: i + 1, + content: + `Lorem ipsum dolor sit amet, consectetur adipisicing elit. Architecto porro commodi cumque ratione sequi reiciendis corrupti a eius praesentium.\n`.repeat( + ((i + 2) % 4) + 1 + ), + })) + return (
+
+ {username ? ( + <> + Logged in as {username} ( + logout()}> + Logout + + ) + + ) : ( + Login + )} +
- {Array.from({ length: 20 }).map(({} = {}, i) => ( -
-
-
Problema {i + 1}
-
-
- {Array.from({ length: ((7 * i) % 4) + 1 }).map(({} = {}) => ( -

- Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo - minima doloremque nihil ducimus qui reiciendis. Provident, odit? -

- ))} -
-
+ {problems.map(p => ( + ))}
diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 821d703..bd4b33f 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,9 +1,41 @@ +import { useState } from 'preact/hooks' import { Link } from '../Router.jsx' export const LoginPage = () => { + const [username, setUsername] = useState('') + + const login = async () => { + await fetch(`/api/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + username, + }), + }) + + location.href = '/#/' + } + return ( -

- Login Page or go back home -

+
+ +
Accedi
+ +
+ + setUsername(e.target.value)} + /> + +
+ +
+
+
) } diff --git a/frontend/src/pages/NewSolution.jsx b/frontend/src/pages/NewSolution.jsx deleted file mode 100644 index 095a346..0000000 --- a/frontend/src/pages/NewSolution.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import { useEffect, useRef, useState } from 'preact/hooks' -import { Markdown } from '../components/Markdown.jsx' - -export const NewSolutionPage = ({ params: { id } }) => { - const [source, setSource] = useState('') - const editorRef = useRef() - - const sendSolution = async () => { - const res = await fetch(`/api/problem/${id}/new-solution`, { - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ - source, - }), - }) - - console.log(await res.json()) - } - - useEffect(() => { - if (editorRef.current) { - editorRef.current.style.height = 'auto' - editorRef.current.style.height = editorRef.current.scrollHeight + 'px' - } - }, [source]) - - return ( -
- - -
Nuova soluzione per il problema #{id}
-
-
-

Editor

- -
-
-

Preview

-
- {source.length ? ( - - ) : ( -
Scrivi una nuova soluzione...
- )} -
-
-
-
- -
-
- ) -} diff --git a/frontend/src/pages/Problem.jsx b/frontend/src/pages/Problem.jsx index 109fa3f..c87c625 100644 --- a/frontend/src/pages/Problem.jsx +++ b/frontend/src/pages/Problem.jsx @@ -1,14 +1,74 @@ -import { Link } from '../Router.jsx' +import { useEffect, useRef, useState } from 'preact/hooks' +import { Markdown } from '../components/Markdown.jsx' +import { Problem } from '../components/Problem.jsx' export const ProblemPage = ({ params: { id }, query }) => { + const [source, setSource] = useState('') + const editorRef = useRef() + + const sendSolution = async () => { + const res = await fetch(`/api/problem/${id}/new-solution`, { + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify({ + source, + }), + }) + + console.log(await res.json()) + } + + useEffect(() => { + if (editorRef.current) { + // settare questo ad "auto" toglie l'altezza al contenitore che passa alla sua + // dimensione minima iniziale, ciĆ² serve per permettere all'autosize della textarea di + // crescere e ridursi ma ha il problema che resetta lo scroll della pagina che deve + // essere preservato a mano + const oldScrollY = window.scrollY + editorRef.current.style.height = 'auto' + editorRef.current.style.height = editorRef.current.scrollHeight + 'px' + window.scrollTo(0, oldScrollY) + } + }, [source]) + return ( - <> -

- Problem {id} with options {JSON.stringify(query)} -

-

- Go back home -

- +
+ +
Testo del problema
+ +
Invia una soluzione al problema
+
+
+

Editor

+ +
+
+

Preview

+
+ {source.trim().length ? ( + + ) : ( +
Scrivi una nuova soluzione...
+ )} +
+
+
+
+ +
+
) } diff --git a/frontend/styles/main.scss b/frontend/styles/main.scss index 34b8079..e943f05 100644 --- a/frontend/styles/main.scss +++ b/frontend/styles/main.scss @@ -1,3 +1,6 @@ +$device-s-width: 640px; +$device-m-width: 1200px; + // Normalize *, @@ -14,11 +17,85 @@ body { margin: 0; font-family: 'Lato', sans-serif; + font-weight: 300; font-size: 20px; line-height: 1; } +textarea { + border: none; + outline: gray; + + width: 100%; + + padding: 1rem; + + box-shadow: -2px 4px 8px 1px #00000020, -1px 1px 1px 0px #00000010; + border-radius: 0.25rem; + background: #ffffff; +} + +label { + font-weight: 400; +} + +input[type='text'] { + border: none; + outline: gray; + + width: 100%; + + padding: 0.5rem; + + box-shadow: -2px 4px 4px 0 #00000020; + border-radius: 0.25rem; + background: #ffffff; + + font-family: 'Lato'; + font-weight: 400; + font-size: 18px; + color: #555; +} + +button { + cursor: pointer; + + font-family: 'Lato'; + font-weight: 600; + font-size: 18px; + color: #555; + + border: 1px solid #c8c8c8; + padding: 0.5rem 2rem; + + box-shadow: -2px 4px 16px 0 #00000010, -2px 4px 4px 0 #00000010; + border-radius: 0.25rem; + + background: linear-gradient(180deg, #f0f0f0, #dfdfdf 20%, #d8d8d8 80%, #c0c0c0); + + transition: all 100ms ease-in; + + &:hover { + border: 1px solid #c4c4c4; + box-shadow: -2px 4px 20px 4px #00000010, -2px 4px 6px 2px #00000010; + background: linear-gradient(180deg, #f8f8f8, #e4e4e4 20%, #e4e4e4 80%, #c8c8c8); + } +} + +.link, +a, +a:visited { + cursor: pointer; + + color: #3a9999; + text-decoration: none; + + &:hover { + text-decoration: underline; + } +} + // Typography p { @@ -60,18 +137,6 @@ $heading-scale: 1.25; } } -$device-s-width: 640px; - -// Not on mobile -@media screen and (min-width: $device-s-width) and (pointer: fine) { - // ... -} - -// On mobile -@media screen and (max-width: $device-s-width), (pointer: coarse) { - // ... -} - // // Components // @@ -99,34 +164,75 @@ body { align-items: center; background: #f8f8f8; +} - main { - padding: 1rem; - width: 100%; +main { + padding: 2rem 2rem 6rem; + width: 100%; - display: flex; - flex-direction: column; - align-items: center; + display: flex; + flex-direction: column; + align-items: center; + + gap: 2rem; + + .logo { + // font-size: 42px; + // font-family: 'EB Garamond'; + // font-weight: 600; + font-size: 38px; + font-family: 'Lato'; + font-weight: 300; } -} -.logo { - // font-size: 42px; - // font-family: 'EB Garamond'; - // font-weight: 600; - font-size: 38px; - font-family: 'Lato'; - font-weight: 300; + .subtitle { + font-size: 24px; + } } main.home { - padding-top: 2rem; - gap: 2rem; + .board { + width: 100%; + + display: grid; + grid-template-columns: repeat(auto-fill, minmax(60ch, 1fr)); + gap: 2rem; + } } -main.new-solution { - padding-top: 2rem; - gap: 2rem; +// +// Components +// + +main { + .problem { + padding: 1rem; + // border: 1px solid #ddd; + + box-shadow: -2px 4px 8px 1px #00000020, -1px 1px 1px 0px #00000010; + border-radius: 0.5rem; + background: #ffffff; + + display: grid; + grid-template-rows: auto 1fr; + gap: 0.5rem; + + max-width: 80ch; + + .problem-header { + display: grid; + grid-template-columns: auto; + + .problem-title { + font-size: 24px; + font-weight: 700; + } + } + + .problem-content { + @extend .text-body; + } + } .solution-editor { display: grid; @@ -154,21 +260,10 @@ main.new-solution { font-family: 'DM Mono', monospace; font-size: 18px; - border: none; - outline: gray; - - width: 100%; - - padding: 1rem; - - box-shadow: -2px 4px 8px 1px #00000020, -1px 1px 1px 0px #00000010; - border-radius: 0.25rem; - background: #ffffff; - resize: none; overflow-y: hidden; - min-height: 10rem; + min-height: 8rem; } } @@ -192,52 +287,63 @@ main.new-solution { } } } -} -.board { - width: 100%; + .form { + min-width: 50ch; - display: grid; - grid-template-columns: repeat(auto-fill, minmax(50ch, 1fr)); - grid-auto-rows: 3rem; - gap: 1rem; -} + display: grid; + grid-template-columns: auto 1fr; -// -// Components -// + // border: 2px solid #ccc; + border-radius: 1rem; -.problem { - padding: 1rem; - // border: 1px solid #ddd; + box-shadow: -2px 4px 16px 4px #00000018, -2px 4px 4px 1px #00000018; - box-shadow: -2px 4px 8px 1px #00000020, -1px 1px 1px 0px #00000010; - border-radius: 0.25rem; - background: #ffffff; + padding: 1.5rem 2rem 1rem; + gap: 1rem; - grid-row: span var(--size); + align-items: center; - overflow-y: auto; + .fill { + grid-column: span 2; + justify-self: center; + } + } +} - display: grid; - grid-template-rows: auto 1fr; - gap: 0.5rem; +.math-inline { + font-size: 95%; +} - .problem-header { - display: grid; - grid-template-columns: auto; +// +// Mobile +// - .problem-title { - font-size: 24px; - font-weight: 700; - } - } +// Not on mobile +@media screen and (min-width: $device-s-width) and (pointer: fine) { + // ... +} - .problem-content { - @extend .text-body; +// On mobile +@media screen and (max-width: $device-s-width), (pointer: coarse) { + main { + .solution-editor { + grid-template-columns: auto; + grid-template-rows: auto auto; + } } } -.math-inline { - font-size: 95%; +@media screen and (max-width: $device-m-width), (pointer: coarse) { + main { + .solution-editor { + grid-template-columns: auto; + grid-template-rows: auto auto; + + .preview, + .editor { + justify-self: center; + } + } + } } diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 0688d30..1a0ce82 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -6,7 +6,7 @@ export default defineConfig({ server: { port: 3000, proxy: { - '/api': 'localhost:4000/api', + '/api': 'http://localhost:4000', }, }, plugins: [preactPlugin()], diff --git a/main.js b/main.js index 5374032..034e87e 100644 --- a/main.js +++ b/main.js @@ -1,29 +1,60 @@ import express from 'express' +import crypto from 'crypto' + // import serveStatic from 'serve-static' import bodyParser from 'body-parser' +import cookieParser from 'cookie-parser' -import { loggingMiddleware, PingRouter, StatusRouter } from './server/helpers.js' +import { authMiddleware, loggingMiddleware, PingRouter, StatusRouter } from './server/helpers.js' import { createDatabase, getUser, updateUser } from './db/database.js' const app = express() +const sessions = { + store: {}, + createSession(username) { + const sid = crypto.randomBytes(10).toString('hex') + this.store[sid] = username + return sid + }, + getUserForSession(sid) { + return this.store[sid] ?? null + }, +} + const db = createDatabase('./db.local.json', { users: { - BachoSeven: { - email: 'foo@bar.com', - password: '123', - }, + ['BachoSeven']: {}, + ['aziis98']: {}, }, problems: {}, }) app.use(bodyParser.json()) +app.use(cookieParser()) + app.use(loggingMiddleware) +app.use(authMiddleware(sid => sessions.getUserForSession(sid))) app.use('/api/status', new StatusRouter()) app.use('/api/ping', new PingRouter()) +app.get('/api/current-user', (req, res) => { + res.json(sessions.getUserForSession(req.cookies.sid)) +}) + +app.post('/api/login', (req, res) => { + const { username } = req.body + res.cookie('sid', sessions.createSession(username), { maxAge: 1000 * 60 * 60 * 24 * 7 }) + res.json({ status: 'ok' }) +}) + +app.post('/api/logout', (req, res) => { + res.cookie('sid', '', { expires: new Date() }) + res.json({ status: 'ok' }) +}) + app.get('/api/user/:id', async (req, res) => { const user = await getUser(db, req.params.id) diff --git a/package.json b/package.json index 017c831..cf997b4 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "body-parser": "^1.20.1", "chalk": "^5.1.2", + "cookie-parser": "^1.4.6", "express": "^4.18.2" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c7de2a..8d9fbee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,12 +4,14 @@ specifiers: body-parser: ^1.20.1 chalk: ^5.1.2 concurrently: ^7.5.0 + cookie-parser: ^1.4.6 express: ^4.18.2 npm-run-all: ^4.1.5 dependencies: body-parser: 1.20.1 chalk: 5.1.2 + cookie-parser: 1.4.6 express: 4.18.2 devDependencies: @@ -175,10 +177,23 @@ packages: engines: {node: '>= 0.6'} dev: false + /cookie-parser/1.4.6: + resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + dev: false + /cookie-signature/1.0.6: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie/0.4.1: + resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} + engines: {node: '>= 0.6'} + dev: false + /cookie/0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} diff --git a/server/helpers.js b/server/helpers.js index 257da4f..5652bb6 100644 --- a/server/helpers.js +++ b/server/helpers.js @@ -41,3 +41,16 @@ export const loggingMiddleware = (req, res, next) => { console.log(`${toLocalISO(new Date())} | ${req.method} ${req.originalUrl} ${coloredStatusCode}`) } + +export const authMiddleware = getUserForSession => async (req, res, next) => { + if (req.cookies.sid) { + req.user = await getUserForSession(req.cookies.sid) + console.log('Request from user: ' + req.user) + } + + next() +} + +export const authenticatedMiddleware = (req, res, next) => { + req.user && next() +}