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 (
PHC / Problemi
+
+ {username ? (
+ <>
+ Logged in as {username} (
+ logout()}>
+ Logout
+
+ )
+ >
+ ) : (
+ Login
+ )}
+
- {Array.from({ length: 20 }).map(({} = {}, i) => (
-
-
-
- {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
-
+
+ PHC / Problemi
+ Accedi
+
+
+
)
}
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 (
-
- PHC / Problemi
-
- 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
-
- >
+
+ PHC / Problemi
+ 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()
+}