diff --git a/backend/src/index.ts b/backend/src/index.ts
index 6aac9a7..b6e34ec 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -4,11 +4,12 @@ import dotenv from 'dotenv'
import init from './init'
import Errors, { authorize } from './functions'
import fs from 'fs'
+import crypto from 'crypto'
dotenv.config()
const prisma = new PrismaClient()
-const app = express()
+const app = express()
fs.mkdirSync("/var/lib/rheinefuerrheine/artikel", { recursive: true })
fs.mkdirSync("/var/lib/rheinefuerrheine/sponsoren", { recursive: true })
@@ -67,13 +68,15 @@ app.get('/api/auth/verify', async (req, res) => {
app.post("/api/article/create", async (req, res) => {
const token = req.headers.authorization?.split(' ')[1]
- if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
- const { title, content, image } = req.body
- if(!title || !content || !image) return res.status(400).send(Errors.MISSING_ITEMS)
+ const { title, content, image, sponsors } = req.body
+ if (!title || !content || !image) return res.status(400).send(Errors.MISSING_ITEMS)
+
+ // sponsors is an array of sponsor IDs
const article = await prisma.articles.create({
data: {
@@ -83,6 +86,11 @@ app.post("/api/article/create", async (req, res) => {
ID: user.ID
}
},
+ sponsors: {
+ connect: sponsors.map((sponsor: string) => ({
+ ID: sponsor
+ }))
+ }
}
})
@@ -95,13 +103,15 @@ app.post("/api/article/create", async (req, res) => {
app.post("/api/article/edit/:id", async (req, res) => {
const token = req.headers.authorization?.split(' ')[1]
- if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
- const { title, content, image } = req.body
- if(!title || !content || !image || !req.params.id) return res.status(400).send(Errors.MISSING_ITEMS)
+ const { title, content, image, sponsors } = req.body
+ if (!title || !content || !image || !req.params.id) return res.status(400).send(Errors.MISSING_ITEMS)
+
+ // sponsors is an array of sponsor IDs
const article = await prisma.articles.update({
where: {
@@ -109,11 +119,17 @@ app.post("/api/article/edit/:id", async (req, res) => {
},
data: {
title,
+ sponsors: {
+ set: sponsors.map((sponsor: string) => ({
+ ID: sponsor
+ }))
+ }
}
})
+
fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${article.ID}.html`, content)
- fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${article.ID}.banner`, image);
+ if (image !== "stale") fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${article.ID}.banner`, image);
res.send(article)
})
@@ -142,7 +158,9 @@ app.get('/api/article/view/:id', async (req, res) => {
createdAt: true,
updatedAt: true,
}
- })
+ }).catch(() => null)
+
+ if (!article) return res.status(404).send(Errors.NOT_FOUND)
res.send(article)
})
@@ -155,10 +173,10 @@ app.get("/api/article/content/:id", async (req, res) => {
app.get("/api/allArticles", async (req, res) => {
const token = req.headers.authorization?.split(' ')[1]
- if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const articles = await prisma.articles.findMany({
select: {
@@ -218,20 +236,20 @@ app.get('/api/articles/public', async (req, res) => {
app.delete('/api/article/:article', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1]
- if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const article = await prisma.articles.findUnique({
where: {
ID: req.params.article
- },
+ },
select: {
ID: true,
title: true,
views: true,
-
+
author: {
select: {
ID: true,
@@ -244,9 +262,9 @@ app.delete('/api/article/:article', async (req, res) => {
}
})
- if(!article) return res.status(404).send(Errors.NOT_FOUND)
+ if (!article) return res.status(404).send(Errors.NOT_FOUND)
- if((!user.admin && !user.article_manage) && article?.author.ID !== user.ID) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if ((!user.admin && !user.article_manage) && article?.author.ID !== user.ID) return res.status(401).send(Errors.INVALID_CREDENTIALS)
await prisma.articles.delete({
where: {
@@ -255,33 +273,41 @@ app.delete('/api/article/:article', async (req, res) => {
})
fs.rmSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.html`)
- if(fs.existsSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)) fs.rmSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)
+ if (fs.existsSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)) fs.rmSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)
res.send('OK')
})
-app.post('/api/article/banner/:article', async (req, res) => {
- const token = req.headers.authorization?.split(' ')[1]
+// app.post('/api/article/banner/:article', async (req, res) => {
+// const token = req.headers.authorization?.split(' ')[1]
- if(!token || !req.body) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+// if(!token || !req.body) return res.status(401).send(Errors.INVALID_CREDENTIALS)
- const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+// const user = await authorize(token)
+// if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
- fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`, req.body.data);
+// fs.writeFileSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`, req.body.data);
- res.send('OK')
-})
+// res.send('OK')
+// })
app.get('/api/article/banner/:article', async (req, res) => {
- if(!fs.existsSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)) return res.status(404).send(Errors.NOT_FOUND)
+ if (!fs.existsSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)) return res.status(404).send(Errors.NOT_FOUND)
- res.sendFile(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)
+ const content = fs.readFileSync(`/var/lib/rheinefuerrheine/artikel/${req.params.article}.banner`)
+
+ // its a base64 encoded image. The type is appended at the beginning of the string (e.g. data:image/png;base64,....) Extract the type and send it as content type header and the rest as the body
+ const type = content.toString().split(';')[0].split(':')[1]
+ res.setHeader('Content-Type', type)
+ res.send(Buffer.from(content.toString().split(';base64,')[1], 'base64'))
})
-app.get('/api/sponsors/getAll', async (req, res) => {
+app.get('/api/sponsors/', async (req, res) => {
const sponsors = await prisma.sponsors.findMany({
+ orderBy: {
+ addedAt: 'desc'
+ }
})
res.send(sponsors)
@@ -290,19 +316,20 @@ app.get('/api/sponsors/getAll', async (req, res) => {
app.post('/api/sponsors/create', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1]
- if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const { name, description, url, logo, banner } = req.body
- if(!name || !description || !url || !logo || !banner) return res.status(400).send(Errors.MISSING_ITEMS)
+ if (!name || !description || !url || !logo || !banner) return res.status(400).send(Errors.MISSING_ITEMS)
+
const sponsor = await prisma.sponsors.create({
data: {
name,
description,
- url,
+ url
}
})
@@ -315,13 +342,13 @@ app.post('/api/sponsors/create', async (req, res) => {
app.patch('/api/sponsors/edit/:id', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1]
- if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const { name, description, url, logo, banner } = req.body
- if(!name || !description || !url || !logo || !banner) return res.status(400).send(Errors.MISSING_ITEMS)
+ if (!name || !description || !url) return res.status(400).send(Errors.MISSING_ITEMS)
const sponsor = await prisma.sponsors.update({
where: {
@@ -334,8 +361,8 @@ app.patch('/api/sponsors/edit/:id', async (req, res) => {
}
})
- fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.logo`, logo)
- fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.banner`, banner)
+ if (logo) fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.logo`, logo)
+ if (banner) fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.banner`, banner)
res.send(sponsor)
})
@@ -343,10 +370,10 @@ app.patch('/api/sponsors/edit/:id', async (req, res) => {
app.delete('/api/sponsors/delete/:id', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1]
- if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
await prisma.sponsors.delete({
where: {
@@ -360,15 +387,49 @@ app.delete('/api/sponsors/delete/:id', async (req, res) => {
res.send('OK')
})
-app.get('/api/users/all', async (req, res) => {
+app.get('/api/sponsors/:id', async (req, res) => {
+ const sponsor = await prisma.sponsors.findUnique({
+ where: {
+ ID: req.params.id
+ }
+ })
+
+ if (!sponsor) return res.status(404).send(Errors.NOT_FOUND)
+
+ res.send(sponsor)
+})
+
+app.get('/api/sponsors/logo/:id', async (req, res) => {
+ if (!fs.existsSync(`/var/lib/rheinefuerrheine/sponsoren/${req.params.id}.logo`)) return res.status(404).send(Errors.NOT_FOUND)
+
+ const content = fs.readFileSync(`/var/lib/rheinefuerrheine/sponsoren/${req.params.id}.logo`)
+
+ // its a base64 encoded image. The type is appended at the beginning of the string (e.g. data:image/png;base64,....) Extract the type and send it as content type header and the rest as the body
+ const type = content.toString().split(';')[0].split(':')[1]
+ res.setHeader('Content-Type', type)
+ res.send(Buffer.from(content.toString().split(';base64,')[1], 'base64'))
+})
+
+app.get('/api/sponsors/banner/:id', async (req, res) => {
+ if (!fs.existsSync(`/var/lib/rheinefuerrheine/sponsoren/${req.params.id}.banner`)) return res.status(404).send(Errors.NOT_FOUND)
+
+ const content = fs.readFileSync(`/var/lib/rheinefuerrheine/sponsoren/${req.params.id}.banner`)
+
+ // its a base64 encoded image. The type is appended at the beginning of the string (e.g. data:image/png;base64,....) Extract the type and send it as content type header and the rest as the body
+ const type = content.toString().split(';')[0].split(':')[1]
+ res.setHeader('Content-Type', type)
+ res.send(Buffer.from(content.toString().split(';base64,')[1], 'base64'))
+})
+
+app.get('/api/users', async (req, res) => {
const token = req.headers.authorization?.split(' ')[1]
- if(!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const user = await authorize(token)
- if(!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
- if(!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+ if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS)
const users = await prisma.users.findMany({
select: {
@@ -385,7 +446,140 @@ app.get('/api/users/all', async (req, res) => {
res.send(users)
})
-app.listen(process.env.PORT, () => {
+app.get('/api/user/:id', async (req, res) => {
+ const token = req.headers.authorization?.split(' ')[1]
+
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const user = await authorize(token)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const foundUser = await prisma.users.findUnique({
+ where: {
+ ID: req.params.id
+ },
+ select: {
+ ID: true,
+ username: true,
+ admin: true,
+ article_create: true,
+ article_manage: true,
+ sponsor_manage: true,
+ user_manage: true,
+ }
+ })
+
+ if (!foundUser) return res.status(404).send(Errors.NOT_FOUND)
+
+ res.send(foundUser)
+})
+
+app.post('/api/users/create', async (req, res) => {
+ const token = req.headers.authorization?.split(' ')[1]
+
+ if (!token || !req.body) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const user = await authorize(token)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const { username, password, admin, article_create, article_manage, sponsor_manage, user_manage } = req.body
+ if (!username || !password || admin === undefined || article_create === undefined || article_manage === undefined || sponsor_manage === undefined || user_manage === undefined) return res.status(400).send(Errors.MISSING_ITEMS)
+
+ const newUser = await prisma.users.create({
+ data: {
+ username,
+ password,
+ token: crypto.randomBytes(64).toString('hex'),
+ admin,
+ article_create,
+ article_manage,
+ sponsor_manage,
+ user_manage,
+ }
+ })
+
+ res.send(newUser)
+})
+
+app.patch('/api/users/edit/:id', async (req, res) => {
+ const token = req.headers.authorization?.split(' ')[1]
+
+ if (!token || !req.body) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const user = await authorize(token)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const { username, password, admin, article_create, article_manage, sponsor_manage, user_manage } = req.body
+ if (!username || admin === undefined || article_create === undefined || article_manage === undefined || sponsor_manage === undefined || user_manage === undefined) return res.status(400).send(Errors.MISSING_ITEMS)
+
+ const newUser = await prisma.users.update({
+ where: {
+ ID: req.params.id
+ },
+ data: {
+ username,
+ admin,
+ article_create,
+ article_manage,
+ sponsor_manage,
+ user_manage,
+ ...(password && { password })
+ }
+ })
+
+ res.send(newUser)
+})
+
+app.delete('/api/users/delete/:id', async (req, res) => {
+ const token = req.headers.authorization?.split(' ')[1]
+
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const user = await authorize(token)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ if (!user.admin && !user.user_manage) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ await prisma.users.delete({
+ where: {
+ ID: req.params.id
+ }
+ })
+
+ res.send('OK')
+})
+
+app.get('/api/stats', async (req, res) => {
+ const token = req.headers.authorization?.split(' ')[1]
+ if (!token) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const user = await authorize(token)
+ if (!user) return res.status(401).send(Errors.INVALID_CREDENTIALS)
+
+ const articles = await prisma.articles.count()
+ const users = await prisma.users.count()
+ const sponsors = await prisma.sponsors.count()
+ const views = await prisma.articles.aggregate({
+ _sum: {
+ views: true
+ }
+ })
+
+ res.send({
+ articles,
+ users,
+ sponsors,
+ views: views._sum.views
+ })
+})
+
+app.listen(process.env.PORT, () => {
console.log(`Server is running on port ${process.env.PORT}`)
})
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 293b6e4..913fbc1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -13,6 +13,7 @@
"@fontsource-variable/lexend": "^5.0.12",
"@fontsource-variable/overpass": "^5.0.9",
"@mui/icons-material": "^5.14.9",
+ "@mui/lab": "^5.0.0-alpha.154",
"@mui/material": "^5.14.10",
"@types/node": "^16.18.52",
"@types/react": "^18.2.22",
@@ -1948,9 +1949,9 @@
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
},
"node_modules/@babel/runtime": {
- "version": "7.22.15",
- "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz",
- "integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==",
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz",
+ "integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -2542,9 +2543,9 @@
}
},
"node_modules/@floating-ui/react-dom": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz",
- "integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==",
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz",
+ "integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==",
"dependencies": {
"@floating-ui/dom": "^1.5.1"
},
@@ -3383,6 +3384,77 @@
}
}
},
+ "node_modules/@mui/lab": {
+ "version": "5.0.0-alpha.154",
+ "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.154.tgz",
+ "integrity": "sha512-Rrhu8eUknjV6hhPMqq52e/p4/c6rvnu/k0AhysuljsHDZcHThYEZNe1mHFLveQ1RIje2VnJSsgmcNfcZKeOOAg==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.4",
+ "@mui/base": "5.0.0-beta.25",
+ "@mui/system": "^5.14.19",
+ "@mui/types": "^7.2.10",
+ "@mui/utils": "^5.14.19",
+ "clsx": "^2.0.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@emotion/react": "^11.5.0",
+ "@emotion/styled": "^11.3.0",
+ "@mui/material": ">=5.10.11",
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/react": {
+ "optional": true
+ },
+ "@emotion/styled": {
+ "optional": true
+ },
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@mui/lab/node_modules/@mui/base": {
+ "version": "5.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.25.tgz",
+ "integrity": "sha512-Iiv+IcappRRv6IBlknIVmLkXxfp51NEX1+l9f+dIbBuPU4PaRULegr1lCeHKsC45KU5ruxM5xMg4R/de03aJQg==",
+ "dependencies": {
+ "@babel/runtime": "^7.23.4",
+ "@floating-ui/react-dom": "^2.0.4",
+ "@mui/types": "^7.2.10",
+ "@mui/utils": "^5.14.19",
+ "@popperjs/core": "^2.11.8",
+ "clsx": "^2.0.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mui-org"
+ },
+ "peerDependencies": {
+ "@types/react": "^17.0.0 || ^18.0.0",
+ "react": "^17.0.0 || ^18.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@mui/material": {
"version": "5.14.10",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.10.tgz",
@@ -3433,12 +3505,12 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/@mui/private-theming": {
- "version": "5.14.10",
- "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.10.tgz",
- "integrity": "sha512-f67xOj3H06wWDT9xBg7hVL/HSKNF+HG1Kx0Pm23skkbEqD2Ef2Lif64e5nPdmWVv+7cISCYtSuE2aeuzrZe78w==",
+ "version": "5.14.19",
+ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.19.tgz",
+ "integrity": "sha512-U9w39VpXLGVM8wZlUU/47YGTsBSk60ZQRRxQZtdqPfN1N7OVllQeN4cEKZKR8PjqqR3aYRcSciQ4dc6CttRoXQ==",
"dependencies": {
- "@babel/runtime": "^7.22.15",
- "@mui/utils": "^5.14.10",
+ "@babel/runtime": "^7.23.4",
+ "@mui/utils": "^5.14.19",
"prop-types": "^15.8.1"
},
"engines": {
@@ -3446,7 +3518,7 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
@@ -3459,11 +3531,11 @@
}
},
"node_modules/@mui/styled-engine": {
- "version": "5.14.10",
- "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.10.tgz",
- "integrity": "sha512-EJckxmQHrsBvDbFu1trJkvjNw/1R7jfNarnqPSnL+jEQawCkQIqVELWLrlOa611TFtxSJGkdUfCFXeJC203HVg==",
+ "version": "5.14.19",
+ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.19.tgz",
+ "integrity": "sha512-jtj/Pyn/bS8PM7NXdFNTHWZfE3p+vItO4/HoQbUeAv3u+cnWXcTBGHHY/xdIn446lYGFDczTh1YyX8G4Ts0Rtg==",
"dependencies": {
- "@babel/runtime": "^7.22.15",
+ "@babel/runtime": "^7.23.4",
"@emotion/cache": "^11.11.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
@@ -3473,7 +3545,7 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.4.1",
@@ -3490,15 +3562,15 @@
}
},
"node_modules/@mui/system": {
- "version": "5.14.10",
- "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.10.tgz",
- "integrity": "sha512-QQmtTG/R4gjmLiL5ECQ7kRxLKDm8aKKD7seGZfbINtRVJDyFhKChA1a+K2bfqIAaBo1EMDv+6FWNT1Q5cRKjFA==",
+ "version": "5.14.19",
+ "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.19.tgz",
+ "integrity": "sha512-4e3Q+2nx+vgEsd0h5ftxlZGB7XtkkPos/zWqCqnxUs1l/T70s0lF2YNrWHHdSQ7LgtBu0eQ0qweZG2pR7KwkAw==",
"dependencies": {
- "@babel/runtime": "^7.22.15",
- "@mui/private-theming": "^5.14.10",
- "@mui/styled-engine": "^5.14.10",
- "@mui/types": "^7.2.4",
- "@mui/utils": "^5.14.10",
+ "@babel/runtime": "^7.23.4",
+ "@mui/private-theming": "^5.14.19",
+ "@mui/styled-engine": "^5.14.19",
+ "@mui/types": "^7.2.10",
+ "@mui/utils": "^5.14.19",
"clsx": "^2.0.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
@@ -3508,7 +3580,7 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@emotion/react": "^11.5.0",
@@ -3529,11 +3601,11 @@
}
},
"node_modules/@mui/types": {
- "version": "7.2.4",
- "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
- "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
+ "version": "7.2.10",
+ "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.10.tgz",
+ "integrity": "sha512-wX1vbDC+lzF7FlhT6A3ffRZgEoKWPF8VqRoTu4lZwouFX2t90KyCMsgepMw5DxLak1BSp/KP86CmtZttikb/gQ==",
"peerDependencies": {
- "@types/react": "*"
+ "@types/react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -3542,12 +3614,12 @@
}
},
"node_modules/@mui/utils": {
- "version": "5.14.10",
- "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.10.tgz",
- "integrity": "sha512-Rn+vYQX7FxkcW0riDX/clNUwKuOJFH45HiULxwmpgnzQoQr3A0lb+QYwaZ+FAkZrR7qLoHKmLQlcItu6LT0y/Q==",
+ "version": "5.14.19",
+ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.19.tgz",
+ "integrity": "sha512-qAHvTXzk7basbyqPvhgWqN6JbmI2wLB/mf97GkSlz5c76MiKYV6Ffjvw9BjKZQ1YRb8rDX9kgdjRezOcoB91oQ==",
"dependencies": {
- "@babel/runtime": "^7.22.15",
- "@types/prop-types": "^15.7.5",
+ "@babel/runtime": "^7.23.4",
+ "@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
},
@@ -3556,7 +3628,7 @@
},
"funding": {
"type": "opencollective",
- "url": "https://opencollective.com/mui"
+ "url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
@@ -4227,9 +4299,9 @@
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
},
"node_modules/@types/prop-types": {
- "version": "15.7.6",
- "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.6.tgz",
- "integrity": "sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg=="
+ "version": "15.7.11",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
+ "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"node_modules/@types/q": {
"version": "1.5.6",
diff --git a/frontend/package.json b/frontend/package.json
index 53fa68c..8ef5beb 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -8,6 +8,7 @@
"@fontsource-variable/lexend": "^5.0.12",
"@fontsource-variable/overpass": "^5.0.9",
"@mui/icons-material": "^5.14.9",
+ "@mui/lab": "^5.0.0-alpha.154",
"@mui/material": "^5.14.10",
"@types/node": "^16.18.52",
"@types/react": "^18.2.22",
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index 2a12462..ac5bc21 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -1,6 +1,6 @@
import { Box, Divider } from "@mui/material"
import SidebarElement from "./SidebarElement"
-import { Home, Newspaper, Person, Savings } from "@mui/icons-material"
+import { ArrowBack, Home, Logout, Newspaper, Person, Savings } from "@mui/icons-material"
function Sidebar() {
@@ -38,6 +38,7 @@ function Sidebar() {
+
+
+
)
diff --git a/frontend/src/components/SidebarElement.tsx b/frontend/src/components/SidebarElement.tsx
index e81715e..72d4115 100644
--- a/frontend/src/components/SidebarElement.tsx
+++ b/frontend/src/components/SidebarElement.tsx
@@ -1,4 +1,4 @@
-import { Box, SvgIconTypeMap, Typography } from "@mui/material";
+import { Box, SvgIconTypeMap, SxProps, Theme, Typography } from "@mui/material";
import { OverridableComponent } from "@mui/material/OverridableComponent";
import { useLocation, useNavigate } from "react-router-dom";
@@ -6,12 +6,14 @@ function SidebarElement({
Title,
Icon,
Path,
+ sx
}: {
Title: string;
Icon: OverridableComponent> & {
muiName: string;
};
Path: string;
+ sx?: SxProps;
}) {
const navigate = useNavigate();
const location = useLocation();
@@ -38,6 +40,7 @@ function SidebarElement({
gap: "25px",
background: (theme) => theme.palette.primary.main,
},
+ ...sx
}}
onClick={() => navigate(Path)}
>
diff --git a/frontend/src/components/SponsorCard.tsx b/frontend/src/components/SponsorCard.tsx
deleted file mode 100644
index fd22355..0000000
--- a/frontend/src/components/SponsorCard.tsx
+++ /dev/null
@@ -1,157 +0,0 @@
-import { Avatar, Box, Typography } from "@mui/material";
-import { useRef, useState } from "react";
-
-function SponsorCard() {
- const logoRef = useRef(null);
- const bannerRef = useRef(null);
-
- const [logo, setLogo] = useState("");
- const [banner, setBanner] = useState("https://placehold.co/500x150");
-
- return (
- <>
- {
- const file = e.target.files?.[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.readAsDataURL(file);
- reader.onload = () => {
- setLogo(reader.result as string);
- };
- }}
- onAbort={() => {}}
- required
- />
- {
- const file = e.target.files?.[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.readAsDataURL(file);
- reader.onload = () => {
- setBanner(reader.result as string);
- };
- }}
- onAbort={() => {}}
- required
- />
-
-
- bannerRef.current?.click()}
- />
-
- {
- logoRef.current?.click();
- }}
- />
-
-
-
- Felix Orgel
-
-
-
- Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
- nonumy eirmod tempor invidunt u
-
-
-
-
- >
- );
-}
-
-export default SponsorCard;
diff --git a/frontend/src/components/SponsorCardEditable.tsx b/frontend/src/components/SponsorCardEditable.tsx
new file mode 100644
index 0000000..082a105
--- /dev/null
+++ b/frontend/src/components/SponsorCardEditable.tsx
@@ -0,0 +1,270 @@
+import { Avatar, Box, Button, TextField } from "@mui/material";
+import { useEffect, useRef, useState } from "react";
+import { getBaseURL } from "../functions";
+
+function SponsorCardEditable({
+ onSave,
+ edit,
+}: {
+ onSave: (result: {
+ name: string,
+ description: string,
+ logo: string | null,
+ banner: string | null,
+ url: string,
+ }) => void;
+ edit?: {
+ id: string,
+ name: string,
+ description: string,
+ url: string,
+ };
+}) {
+ const logoRef = useRef(null);
+ const bannerRef = useRef(null);
+
+ const [logo, setLogo] = useState("https://placehold.co/150x150");
+ const [banner, setBanner] = useState("https://placehold.co/500x150");
+
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+
+ const [url, setUrl] = useState("");
+
+ useEffect(() => {
+ if (!edit) return;
+
+ setName(edit.name);
+ setDescription(edit.description);
+ setUrl(edit.url);
+ setLogo(`${getBaseURL()}/api/sponsors/logo/${edit.id}`);
+ setBanner(`${getBaseURL()}/api/sponsors/banner/${edit.id}`);
+ }, [edit]);
+
+ const valid = () => {
+ if (!name) return false;
+ if (!description) return false;
+ if (!logo) return false;
+ if (!banner) return false;
+ if (!url) return false;
+
+ if (logo.startsWith("https://placehold.co/")) return false;
+ if (banner.startsWith("https://placehold.co/")) return false;
+
+ return true;
+ };
+
+ return (
+ <>
+ {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => {
+ setLogo(reader.result as string);
+ };
+ }}
+ onAbort={() => {}}
+ required
+ />
+ {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => {
+ setBanner(reader.result as string);
+ };
+ }}
+ onAbort={() => {}}
+ required
+ />
+
+
+ bannerRef.current?.click()}
+ />
+
+ {
+ logoRef.current?.click();
+ }}
+ />
+
+
+ {
+ if (e.target.value.length >= 12) return;
+ if (e.target.value.includes("\n")) return;
+ setName(e.target.value);
+ }}
+ />
+
+ {
+ if (e.target.value.length >= 100) return;
+ if (e.target.value.includes("\n")) return;
+ setDescription(e.target.value);
+ }}
+ />
+
+
+
+
+ {
+ if (e.target.value.length >= 128) return;
+ if (e.target.value.includes("\n")) return;
+ setUrl(e.target.value);
+ }}
+ />
+
+
+ >
+ );
+}
+
+export default SponsorCardEditable;
diff --git a/frontend/src/components/SponsorImageSmall.tsx b/frontend/src/components/SponsorImageSmall.tsx
new file mode 100644
index 0000000..7d4f4e2
--- /dev/null
+++ b/frontend/src/components/SponsorImageSmall.tsx
@@ -0,0 +1,53 @@
+import { Box, Grid, Avatar } from "@mui/material";
+
+export function SponsorImageSmall({
+ name,
+ image,
+ description,
+ link,
+ onRemove,
+}: {
+ name: string;
+ image: string;
+ description: string;
+ link: string;
+ onRemove?: () => void;
+}): JSX.Element {
+ return (
+
+
+ {
+ window.open(link, "_blank");
+ })
+ }
+ />
+
+
+ );
+}
diff --git a/frontend/src/components/SponsorModal.tsx b/frontend/src/components/SponsorModal.tsx
index 322e542..02c4ce0 100644
--- a/frontend/src/components/SponsorModal.tsx
+++ b/frontend/src/components/SponsorModal.tsx
@@ -2,12 +2,14 @@ import { Close } from "@mui/icons-material";
import {
Backdrop,
Box,
+ CircularProgress,
IconButton,
- TextField,
Typography,
} from "@mui/material";
-import { useState } from "react";
-import SponsorCard from "./SponsorCard";
+import { useEffect, useState } from "react";
+import SponsorCardEditable from "./SponsorCardEditable";
+import axios from "axios";
+import { getBaseURL } from "../functions";
function SponsorModal({
id,
@@ -16,64 +18,192 @@ function SponsorModal({
id: string | null;
onClose?: () => void;
}) {
+ const [saving, setSaving] = useState(false);
+ const [loading, setLoading] = useState(false);
+ const [data, setData] = useState(null);
+
+ useEffect(() => {
+ setData(null);
+ if(!id) return;
+ if (id === "create") return;
+ setLoading(true);
+
+ axios
+ .get(`${getBaseURL()}/api/sponsors/${id}`)
+ .then((res) => {
+ setData(res.data);
+ setLoading(false);
+ })
+ .catch(() => {
+ onClose?.();
+ });
+ }, [id]);
+
if (!id) return <>>;
- return (
-
-
+
+
+ Lade...
+
+
+ );
+ return (
+ <>
+ {saving && (
+
+
+
+ Speichere...
+
+
+ )}
+
-
- Sponsor Erstellen
-
-
-
-
-
+
+
+ Sponsor { id === "create" ? "Erstellen" : "Bearbeiten" }
+
+
+
+
+
-
-
-
+ {
+ setSaving(true);
+ if (id === "create") {
+ axios
+ .post(
+ `${getBaseURL()}/api/sponsors/create`,
+ {
+ name: result.name,
+ description: result.description,
+ url: result.url,
+ logo: result.logo,
+ banner: result.banner,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem(
+ "token"
+ )}`,
+ },
+ }
+ )
+ .then(() => {
+ window.location.reload();
+ });
+ } else {
+ result.logo = result.logo?.includes("http")
+ ? null
+ : result.logo;
+ result.banner = result.banner?.includes("http")
+ ? null
+ : result.banner;
+
+ axios
+ .patch(
+ `${getBaseURL()}/api/sponsors/edit/${id}`,
+ {
+ name: result.name,
+ description: result.description,
+ url: result.url,
+ logo: result.logo,
+ banner: result.banner,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem(
+ "token"
+ )}`,
+ },
+ }
+ )
+ .then(() => {
+ window.location.reload();
+ });
+ }
+ }}
+ />
+
+
+ >
);
}
diff --git a/frontend/src/components/UserModal.tsx b/frontend/src/components/UserModal.tsx
new file mode 100644
index 0000000..e6cacae
--- /dev/null
+++ b/frontend/src/components/UserModal.tsx
@@ -0,0 +1,382 @@
+import { Close } from "@mui/icons-material";
+import {
+ Backdrop,
+ Box,
+ Button,
+ Checkbox,
+ CircularProgress,
+ IconButton,
+ Stack,
+ TextField,
+ Typography,
+} from "@mui/material";
+import { useEffect, useState } from "react";
+import axios from "axios";
+import { getBaseURL } from "../functions";
+import { LoadingButton } from "@mui/lab";
+import { sha256 } from "js-sha256";
+
+function UserModal({
+ id,
+ onClose,
+}: {
+ id: string | null;
+ onClose?: () => void;
+}) {
+ const [saving, setSaving] = useState(false);
+ const [loading, setLoading] = useState(false);
+
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+
+ const [permissions, setPermissions] = useState({
+ admin: false,
+ article_create: false,
+ article_manage: false,
+ sponsor_manage: false,
+ user_manage: false,
+ });
+
+ const valid = () => {
+ if (!username) return false;
+ if (!password && id === "create") return false;
+
+ return true;
+ };
+
+ useEffect(() => {
+ setUsername("");
+ setPassword("");
+ setPermissions({
+ admin: false,
+ article_create: false,
+ article_manage: false,
+ sponsor_manage: false,
+ user_manage: false,
+ });
+
+ if (!id) return;
+ if (id === "create") return;
+ setLoading(true);
+
+ axios
+ .get(`${getBaseURL()}/api/user/${id}`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ },
+ })
+ .then((res) => {
+ setUsername(res.data.username);
+
+ setPermissions({
+ admin: res.data.admin,
+ article_create: res.data.article_create,
+ article_manage: res.data.article_manage,
+ sponsor_manage: res.data.sponsor_manage,
+ user_manage: res.data.user_manage,
+ });
+
+ setLoading(false);
+ });
+ }, [id]);
+
+ if (!id) return <>>;
+ if (loading)
+ return (
+
+
+
+ Lade...
+
+
+ );
+ return (
+ <>
+
+
+
+
+
+ Benutzer {id === "create" ? "Erstellen" : "Bearbeiten"}
+
+
+
+
+
+
+ {
+ if (e.target.value.length >= 64) return;
+ if (e.target.value.includes("\n")) return;
+ setUsername(e.target.value);
+ }}
+ sx={{
+ width: "80%",
+ }}
+ />
+ {
+ if (e.target.value.length >= 64) return;
+ if (e.target.value.includes("\n")) return;
+ setPassword(e.target.value);
+ }}
+ sx={{
+ width: "80%",
+ }}
+ />
+
+
+
+ {
+ setPermissions({
+ ...permissions,
+ admin: e.target.checked,
+ });
+ }}
+ />
+ Admin
+
+
+
+ {
+ setPermissions({
+ ...permissions,
+ article_create: e.target.checked,
+ });
+ }}
+ />
+ Artikel Erstellen
+
+
+
+ {
+ setPermissions({
+ ...permissions,
+ article_manage: e.target.checked,
+ });
+ }}
+ />
+ Artikel Verwalten
+
+
+
+ {
+ setPermissions({
+ ...permissions,
+ sponsor_manage: e.target.checked,
+ });
+ }}
+ />
+ Sponsoren Verwalten
+
+
+
+ {
+ setPermissions({
+ ...permissions,
+ user_manage: e.target.checked,
+ });
+ }}
+ />
+ Nutzer Verwalten
+
+
+
+ {
+ setSaving(true);
+
+ if (id === "create") {
+ axios
+ .post(
+ `${getBaseURL()}/api/users/create`,
+ {
+ username,
+ password: sha256(`rheine ${password.trim()} rheine`),
+ admin: permissions.admin,
+ article_create: permissions.admin
+ ? false
+ : permissions.article_create,
+ article_manage: permissions.admin
+ ? false
+ : permissions.article_manage,
+ sponsor_manage: permissions.admin
+ ? false
+ : permissions.sponsor_manage,
+ user_manage: permissions.admin
+ ? false
+ : permissions.user_manage,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem(
+ "token"
+ )}`,
+ },
+ }
+ )
+ .then(() => {
+ setSaving(false);
+ onClose?.();
+ });
+ } else {
+ axios
+ .patch(
+ `${getBaseURL()}/api/users/edit/${id}`,
+ {
+ username,
+ password: password
+ ? sha256(`rheine ${password.trim()} rheine`)
+ : undefined,
+ admin: permissions.admin,
+ article_create: permissions.admin
+ ? false
+ : permissions.article_create,
+ article_manage: permissions.admin
+ ? false
+ : permissions.article_manage,
+ sponsor_manage: permissions.admin
+ ? false
+ : permissions.sponsor_manage,
+ user_manage: permissions.admin
+ ? false
+ : permissions.user_manage,
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem(
+ "token"
+ )}`,
+ },
+ }
+ )
+ .then(() => {
+ setSaving(false);
+ onClose?.();
+ });
+ }
+ }}
+ loading={saving}
+ >
+ Speichern
+
+
+
+ >
+ );
+}
+
+export default UserModal;
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 0f9443a..f615158 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -125,3 +125,6 @@ a {
text-decoration: none;
}
+input[name="suneditor_image_radio"] {
+ display: none;
+}
\ No newline at end of file
diff --git a/frontend/src/pages/AdminFrame.tsx b/frontend/src/pages/AdminFrame.tsx
index df246a5..a93913b 100644
--- a/frontend/src/pages/AdminFrame.tsx
+++ b/frontend/src/pages/AdminFrame.tsx
@@ -6,6 +6,7 @@ import Artikel from "./admin/Artikel";
import Sponsoren from "./admin/Sponsoren";
import Benutzer from "./admin/Benutzer";
import ArticleEditor from "./admin/ArticleEditor";
+import Logout from "./admin/Logout";
function AdminFrame() {
return (
@@ -34,6 +35,7 @@ function AdminFrame() {
} />
} />
} />
+ } />
diff --git a/frontend/src/pages/Artikel.tsx b/frontend/src/pages/Artikel.tsx
index 461ddc5..1d66257 100644
--- a/frontend/src/pages/Artikel.tsx
+++ b/frontend/src/pages/Artikel.tsx
@@ -1,12 +1,20 @@
/* eslint-disable jsx-a11y/alt-text */
import { useParams } from "react-router-dom";
import TopBar from "../components/TopBar";
-import { Box, CircularProgress, Grid, Typography } from "@mui/material";
+import {
+ Box,
+ CircularProgress,
+ Grid,
+ Tooltip,
+ Typography,
+} from "@mui/material";
import { getBaseURL } from "../functions";
import { useEffect, useRef, useState } from "react";
import axios from "axios";
import "./Artikel.css";
+import "suneditor/dist/css/suneditor.min.css";
+import { SponsorImageSmall } from "../components/SponsorImageSmall";
function Artikel() {
const ref = useRef(null);
@@ -16,25 +24,10 @@ function Artikel() {
const [loadingContent, setLoadingContent] = useState(true);
const [article, setArticle] = useState(null);
- const [banner, setBanner] = useState("");
-
- const loadbanner = () => {
- axios
- .get(`${getBaseURL()}/api/article/banner/${id}`)
- .then((response) => {
- setBanner(response.data);
- })
- .catch((err) => {
- console.log(err);
- setBanner("https://placehold.co/1920x1080");
- });
- };
useEffect(() => {
if (!id) return;
- loadbanner();
-
axios.get(`${getBaseURL()}/api/article/content/${id}`).then((response) => {
if (ref.current) {
ref.current.innerHTML = response.data;
@@ -56,7 +49,7 @@ function Artikel() {
}
}, 500);
});
- // eslint-disable-next-line react-hooks/exhaustive-deps
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);
return (
@@ -64,20 +57,22 @@ function Artikel() {
{loadingContent && }
{!loadingContent && (
-
+
-
+
+ {article?.sponsors.map((sponsor) => (
+
+
+
+ ))}
+
@@ -170,7 +237,7 @@ function ArticleEditor() {
accept="image/*"
style={{ display: "none" }}
onChange={(e) => {
- const file = e.target.files?.[0]
+ const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
@@ -183,10 +250,11 @@ function ArticleEditor() {
required
/>
-
-
+
- Lorem ipsum whatever
+ Titel
-
-
+ {sponsorsSelected.map((sponsor) => (
+ {
+ setSponsorsSelected(
+ sponsorsSelected.filter((e) => e.ID !== sponsor.ID)
+ );
+ }}
+ />
+ ))}
{}}
+ onClick={(e) => {
+ setSponsorMenuEl(e.currentTarget as any);
+ }}
/>
@@ -354,28 +427,4 @@ function ArticleEditor() {
export default ArticleEditor;
-function SponsorImageSmall({
- name,
- image,
- description,
- link,
-}: {
- name: string;
- image: string;
- description: string;
- link: string;
-}): JSX.Element {
- return (
-
-
-
-
-
- );
-}
+
diff --git a/frontend/src/pages/admin/Benutzer.tsx b/frontend/src/pages/admin/Benutzer.tsx
index ab6b13e..900bced 100644
--- a/frontend/src/pages/admin/Benutzer.tsx
+++ b/frontend/src/pages/admin/Benutzer.tsx
@@ -1,8 +1,4 @@
-import {
- ManageAccounts,
- PersonAdd,
- PersonRemove,
-} from "@mui/icons-material";
+import { ManageAccounts, PersonAdd, PersonRemove } from "@mui/icons-material";
import {
Box,
Button,
@@ -17,17 +13,17 @@ import {
} from "@mui/material";
import axios from "axios";
import { useEffect, useState } from "react";
-import { useNavigate } from "react-router-dom";
import { getBaseURL } from "../../functions";
+import UserModal from "../../components/UserModal";
function Benutzer() {
- const navigate = useNavigate();
+ const [modalID, setModelID] = useState(null);
const [users, setUsers] = useState([]);
useEffect(() => {
axios
- .get(`${getBaseURL()}/api/users/all`, {
+ .get(`${getBaseURL()}/api/users`, {
headers: {
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
@@ -41,140 +37,165 @@ function Benutzer() {
}, []);
return (
-
+ <>
+ {
+ setModelID(null);
+ }}
+ />
-
- Benutzer
-
-
-
-
+
+ Benutzer
+
-
+
+
-
-
-
-
- Name
-
- Berechtigungen
- Optionen
-
-
-
- {users.map((user) => (
+
+
+
+
- {user.username}
-
- {[
- { name: "Admin", value: user.admin },
- { name: "Artikel Erstellen", value: user.article_create },
- { name: "Artikel Verwalten", value: user.article_manage },
- { name: "Sponsoren Verwalten", value: user.sponsor_manage },
- { name: "Nutzer Verwalten", value: user.user_manage },
- ]
- .filter((e) => e.value)
- .map((e) => e.name)
- .join(", ")}
-
-
-
-
-
-
-
-
-
-
-
-
+
+ Name
+ Berechtigungen
+
- ))}
-
-
-
+
+
+ {users.map((user) => (
+
+ {user.username}
+
+ {[
+ { name: "Admin", value: user.admin },
+ { name: "Artikel Erstellen", value: user.article_create },
+ { name: "Artikel Verwalten", value: user.article_manage },
+ { name: "Sponsoren Verwalten", value: user.sponsor_manage },
+ { name: "Nutzer Verwalten", value: user.user_manage },
+ ]
+ .filter((e) => e.value)
+ .map((e) => e.name)
+ .join(", ")}
+
+
+
+ {
+ setModelID(user.ID);
+ }}
+ >
+
+
+
+ {
+ axios
+ .delete(`${getBaseURL()}/api/users/delete/${user.ID}`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem(
+ "token"
+ )}`,
+ },
+ })
+ .then(() => {
+ setUsers(
+ users.filter((e) => e.ID !== user.ID)
+ );
+ });
+ }}
+ >
+
+
+
+
+
+ ))}
+
+
+
+ >
);
}
diff --git a/frontend/src/pages/admin/Dashboard.tsx b/frontend/src/pages/admin/Dashboard.tsx
index 3be1194..febb959 100644
--- a/frontend/src/pages/admin/Dashboard.tsx
+++ b/frontend/src/pages/admin/Dashboard.tsx
@@ -1,8 +1,34 @@
import { Visibility, Newspaper, Person, Savings } from "@mui/icons-material";
import { Box, Grid, SvgIconTypeMap, Typography } from "@mui/material";
import { OverridableComponent } from "@mui/material/OverridableComponent";
+import axios from "axios";
+import { useEffect, useState } from "react";
+import { getBaseURL } from "../../functions";
function Dashboard() {
+ const [stats, setStats] = useState<{
+ users: number;
+ articles: number;
+ sponsors: number;
+ views: number;
+ }>({
+ users: 0,
+ articles: 0,
+ sponsors: 0,
+ views: 0,
+ });
+
+ useEffect(() => {
+ axios.get(`${getBaseURL()}/api/stats`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem("token")}`,
+ },
+ }).then((response) => {
+ console.log(response.data);
+ setStats(response.data);
+ });
+ }, []);
+
return (
-
-
-
-
+
+
+
+
);
@@ -49,7 +75,8 @@ function StatDisplay({
flexDirection: "column",
padding: "10px",
- background: (theme) => `linear-gradient(45deg, ${theme.palette.secondary.light}, ${theme.palette.secondary.main})`,
+ background: (theme) =>
+ `linear-gradient(45deg, ${theme.palette.secondary.light}, ${theme.palette.secondary.main})`,
width: "100%",
}}
@@ -74,14 +101,16 @@ function StatDisplay({
{Title}
-
+ padding: "5px",
+ backgroundColor: "#fff",
+ borderRadius: "5px",
+ }}
+ >
-
-
- {Value}
-
+ }}
+ >
+
+ {Value}
+
diff --git a/frontend/src/pages/admin/Logout.tsx b/frontend/src/pages/admin/Logout.tsx
new file mode 100644
index 0000000..a300bb2
--- /dev/null
+++ b/frontend/src/pages/admin/Logout.tsx
@@ -0,0 +1,31 @@
+import { Backdrop, CircularProgress, Typography } from "@mui/material"
+import { useEffect } from "react"
+
+function Logout() {
+ useEffect(() => {
+ localStorage.removeItem("token")
+ setTimeout(() => {
+ window.location.href = "/login"
+ }, 1000)
+ }, [])
+
+ return (
+
+
+
+ Logge aus...
+
+
+ )
+}
+
+export default Logout
\ No newline at end of file
diff --git a/frontend/src/pages/admin/Sponsoren.tsx b/frontend/src/pages/admin/Sponsoren.tsx
index a35c56d..d7480dc 100644
--- a/frontend/src/pages/admin/Sponsoren.tsx
+++ b/frontend/src/pages/admin/Sponsoren.tsx
@@ -15,21 +15,51 @@ import {
TableRow,
Typography,
Tooltip,
+ Avatar,
+ Backdrop,
+ CircularProgress,
} from "@mui/material";
-import { useNavigate } from "react-router-dom";
import SponsorModal from "../../components/SponsorModal";
-import { useState } from "react";
+import { useEffect, useState } from "react";
+import axios from "axios";
+import { getBaseURL } from "../../functions";
+import moment from "moment";
function Sponsoren() {
- const navigate = useNavigate();
-
const [modalID, setModelID] = useState(null);
+ const [sponsors, setSponsors] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ setLoaded(false);
+ axios.get(`${getBaseURL()}/api/sponsors/`).then((res) => {
+ setSponsors(res.data);
+ setLoaded(true);
+ });
+ }, []);
+
return (
<>
- {
- setModelID(null);
- }} />
+
+
+
+ {
+ setModelID(null);
+ }}
+ />
-
+
+ {" "}
+
+
Name
- Ansprechpartner
Datum
Optionen
-
- RF Computer GMBH
- R. Fink
-
- 7 Nov. 2023
-
-
-
-
- (
+
+
+
+
+ {sponsor.name}
+
+ {moment(sponsor.addedAt).locale("de").format("ll")}
+
+
+
+
+
-
-
-
+ padding: "5px",
+ backgroundColor: "#fff",
+ borderRadius: "5px",
+ }}
+ onClick={() => {
+ setModelID(sponsor.ID);
+ }}
+ >
+
+
+
-
-
+
-
-
-
-
-
-
+ padding: "5px",
+ backgroundColor: "#fff",
+ borderRadius: "5px",
+ "&:hover": {
+ background: "red",
+ },
+ }}
+ onClick={() => {
+ axios
+ .delete(
+ `${getBaseURL()}/api/sponsors/delete/${
+ sponsor.ID
+ }`,
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem(
+ "token"
+ )}`,
+ },
+ }
+ )
+ .then(() => {
+ setSponsors(
+ sponsors.filter((s) => s.ID !== sponsor.ID)
+ );
+ });
+ }}
+ >
+
+
+
+
+
+
+ ))}
diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts
index 6ab8b21..ff38fb3 100644
--- a/frontend/src/types.d.ts
+++ b/frontend/src/types.d.ts
@@ -7,22 +7,28 @@ declare namespace Types {
ID: string;
username: string;
}
- sponsors: {
- ID: string;
- name: string;
- url: string;
- description: string;
- }[];
+ sponsors: Sponsor[];
updatedAt: Date;
createdAt: Date;
}
- interface User {
+ interface User extends UserPermissions {
ID: string;
username: string;
+ }
+
+ interface UserPermissions {
admin: boolean;
article_create: boolean;
article_manage: boolean;
sponsor_manage: boolean;
user_manage: boolean;
}
+
+ interface Sponsor {
+ ID: string;
+ name: string;
+ url: string;
+ description: string;
+ addedAt: Date;
+ }
}
\ No newline at end of file