Built a fckn cdn
This commit is contained in:
parent
543aca02ba
commit
5ce485e924
|
@ -4,6 +4,7 @@ import dotenv from 'dotenv'
|
||||||
import init from './init'
|
import init from './init'
|
||||||
import Errors, { authorize } from './functions'
|
import Errors, { authorize } from './functions'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
|
|
||||||
|
@ -72,9 +73,11 @@ app.post("/api/article/create", async (req, res) => {
|
||||||
const user = await authorize(token)
|
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
|
const { title, content, image, sponsors } = req.body
|
||||||
if (!title || !content || !image) return res.status(400).send(Errors.MISSING_ITEMS)
|
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({
|
const article = await prisma.articles.create({
|
||||||
data: {
|
data: {
|
||||||
title,
|
title,
|
||||||
|
@ -83,6 +86,11 @@ app.post("/api/article/create", async (req, res) => {
|
||||||
ID: user.ID
|
ID: user.ID
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
sponsors: {
|
||||||
|
connect: sponsors.map((sponsor: string) => ({
|
||||||
|
ID: sponsor
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -100,20 +108,28 @@ app.post("/api/article/edit/:id", async (req, res) => {
|
||||||
const user = await authorize(token)
|
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
|
const { title, content, image, sponsors } = req.body
|
||||||
if (!title || !content || !image || !req.params.id) return res.status(400).send(Errors.MISSING_ITEMS)
|
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({
|
const article = await prisma.articles.update({
|
||||||
where: {
|
where: {
|
||||||
ID: req.params.id
|
ID: req.params.id
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
title,
|
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}.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)
|
res.send(article)
|
||||||
})
|
})
|
||||||
|
@ -142,7 +158,9 @@ app.get('/api/article/view/:id', async (req, res) => {
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
updatedAt: true,
|
updatedAt: true,
|
||||||
}
|
}
|
||||||
})
|
}).catch(() => null)
|
||||||
|
|
||||||
|
if (!article) return res.status(404).send(Errors.NOT_FOUND)
|
||||||
|
|
||||||
res.send(article)
|
res.send(article)
|
||||||
})
|
})
|
||||||
|
@ -260,28 +278,36 @@ app.delete('/api/article/:article', async (req, res) => {
|
||||||
res.send('OK')
|
res.send('OK')
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/api/article/banner/:article', async (req, res) => {
|
// app.post('/api/article/banner/:article', async (req, res) => {
|
||||||
const token = req.headers.authorization?.split(' ')[1]
|
// 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)
|
// 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)
|
||||||
|
|
||||||
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) => {
|
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({
|
const sponsors = await prisma.sponsors.findMany({
|
||||||
|
orderBy: {
|
||||||
|
addedAt: 'desc'
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
res.send(sponsors)
|
res.send(sponsors)
|
||||||
|
@ -298,11 +324,12 @@ app.post('/api/sponsors/create', async (req, res) => {
|
||||||
const { name, description, url, logo, banner } = req.body
|
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({
|
const sponsor = await prisma.sponsors.create({
|
||||||
data: {
|
data: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
url,
|
url
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -321,7 +348,7 @@ app.patch('/api/sponsors/edit/:id', async (req, res) => {
|
||||||
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
|
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({
|
const sponsor = await prisma.sponsors.update({
|
||||||
where: {
|
where: {
|
||||||
|
@ -334,8 +361,8 @@ app.patch('/api/sponsors/edit/:id', async (req, res) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.logo`, logo)
|
if (logo) fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.logo`, logo)
|
||||||
fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.banner`, banner)
|
if (banner) fs.writeFileSync(`/var/lib/rheinefuerrheine/sponsoren/${sponsor.ID}.banner`, banner)
|
||||||
|
|
||||||
res.send(sponsor)
|
res.send(sponsor)
|
||||||
})
|
})
|
||||||
|
@ -360,7 +387,41 @@ app.delete('/api/sponsors/delete/:id', async (req, res) => {
|
||||||
res.send('OK')
|
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]
|
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)
|
||||||
|
@ -385,6 +446,139 @@ app.get('/api/users/all', async (req, res) => {
|
||||||
res.send(users)
|
res.send(users)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
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, () => {
|
app.listen(process.env.PORT, () => {
|
||||||
console.log(`Server is running on port ${process.env.PORT}`)
|
console.log(`Server is running on port ${process.env.PORT}`)
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
"@fontsource-variable/lexend": "^5.0.12",
|
"@fontsource-variable/lexend": "^5.0.12",
|
||||||
"@fontsource-variable/overpass": "^5.0.9",
|
"@fontsource-variable/overpass": "^5.0.9",
|
||||||
"@mui/icons-material": "^5.14.9",
|
"@mui/icons-material": "^5.14.9",
|
||||||
|
"@mui/lab": "^5.0.0-alpha.154",
|
||||||
"@mui/material": "^5.14.10",
|
"@mui/material": "^5.14.10",
|
||||||
"@types/node": "^16.18.52",
|
"@types/node": "^16.18.52",
|
||||||
"@types/react": "^18.2.22",
|
"@types/react": "^18.2.22",
|
||||||
|
@ -1948,9 +1949,9 @@
|
||||||
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
|
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
|
||||||
},
|
},
|
||||||
"node_modules/@babel/runtime": {
|
"node_modules/@babel/runtime": {
|
||||||
"version": "7.22.15",
|
"version": "7.23.5",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.15.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz",
|
||||||
"integrity": "sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==",
|
"integrity": "sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"regenerator-runtime": "^0.14.0"
|
"regenerator-runtime": "^0.14.0"
|
||||||
},
|
},
|
||||||
|
@ -2542,9 +2543,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/react-dom": {
|
"node_modules/@floating-ui/react-dom": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.4.tgz",
|
||||||
"integrity": "sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==",
|
"integrity": "sha512-CF8k2rgKeh/49UrnIBs4BdxPUV6vize/Db1d/YbCLyp9GiVZ0BEwf5AiDSxJRCr6yOkGqTFHtmrULxkEfYZ7dQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/dom": "^1.5.1"
|
"@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": {
|
"node_modules/@mui/material": {
|
||||||
"version": "5.14.10",
|
"version": "5.14.10",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.10.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.10.tgz",
|
||||||
|
@ -3433,12 +3505,12 @@
|
||||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
||||||
},
|
},
|
||||||
"node_modules/@mui/private-theming": {
|
"node_modules/@mui/private-theming": {
|
||||||
"version": "5.14.10",
|
"version": "5.14.19",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.10.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.19.tgz",
|
||||||
"integrity": "sha512-f67xOj3H06wWDT9xBg7hVL/HSKNF+HG1Kx0Pm23skkbEqD2Ef2Lif64e5nPdmWVv+7cISCYtSuE2aeuzrZe78w==",
|
"integrity": "sha512-U9w39VpXLGVM8wZlUU/47YGTsBSk60ZQRRxQZtdqPfN1N7OVllQeN4cEKZKR8PjqqR3aYRcSciQ4dc6CttRoXQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.15",
|
"@babel/runtime": "^7.23.4",
|
||||||
"@mui/utils": "^5.14.10",
|
"@mui/utils": "^5.14.19",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -3446,7 +3518,7 @@
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/mui"
|
"url": "https://opencollective.com/mui-org"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^17.0.0 || ^18.0.0",
|
"@types/react": "^17.0.0 || ^18.0.0",
|
||||||
|
@ -3459,11 +3531,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/styled-engine": {
|
"node_modules/@mui/styled-engine": {
|
||||||
"version": "5.14.10",
|
"version": "5.14.19",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.10.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.19.tgz",
|
||||||
"integrity": "sha512-EJckxmQHrsBvDbFu1trJkvjNw/1R7jfNarnqPSnL+jEQawCkQIqVELWLrlOa611TFtxSJGkdUfCFXeJC203HVg==",
|
"integrity": "sha512-jtj/Pyn/bS8PM7NXdFNTHWZfE3p+vItO4/HoQbUeAv3u+cnWXcTBGHHY/xdIn446lYGFDczTh1YyX8G4Ts0Rtg==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.15",
|
"@babel/runtime": "^7.23.4",
|
||||||
"@emotion/cache": "^11.11.0",
|
"@emotion/cache": "^11.11.0",
|
||||||
"csstype": "^3.1.2",
|
"csstype": "^3.1.2",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
|
@ -3473,7 +3545,7 @@
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/mui"
|
"url": "https://opencollective.com/mui-org"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
|
@ -3490,15 +3562,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/system": {
|
"node_modules/@mui/system": {
|
||||||
"version": "5.14.10",
|
"version": "5.14.19",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.10.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.19.tgz",
|
||||||
"integrity": "sha512-QQmtTG/R4gjmLiL5ECQ7kRxLKDm8aKKD7seGZfbINtRVJDyFhKChA1a+K2bfqIAaBo1EMDv+6FWNT1Q5cRKjFA==",
|
"integrity": "sha512-4e3Q+2nx+vgEsd0h5ftxlZGB7XtkkPos/zWqCqnxUs1l/T70s0lF2YNrWHHdSQ7LgtBu0eQ0qweZG2pR7KwkAw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.15",
|
"@babel/runtime": "^7.23.4",
|
||||||
"@mui/private-theming": "^5.14.10",
|
"@mui/private-theming": "^5.14.19",
|
||||||
"@mui/styled-engine": "^5.14.10",
|
"@mui/styled-engine": "^5.14.19",
|
||||||
"@mui/types": "^7.2.4",
|
"@mui/types": "^7.2.10",
|
||||||
"@mui/utils": "^5.14.10",
|
"@mui/utils": "^5.14.19",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"csstype": "^3.1.2",
|
"csstype": "^3.1.2",
|
||||||
"prop-types": "^15.8.1"
|
"prop-types": "^15.8.1"
|
||||||
|
@ -3508,7 +3580,7 @@
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/mui"
|
"url": "https://opencollective.com/mui-org"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emotion/react": "^11.5.0",
|
"@emotion/react": "^11.5.0",
|
||||||
|
@ -3529,11 +3601,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/types": {
|
"node_modules/@mui/types": {
|
||||||
"version": "7.2.4",
|
"version": "7.2.10",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.10.tgz",
|
||||||
"integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==",
|
"integrity": "sha512-wX1vbDC+lzF7FlhT6A3ffRZgEoKWPF8VqRoTu4lZwouFX2t90KyCMsgepMw5DxLak1BSp/KP86CmtZttikb/gQ==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "*"
|
"@types/react": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@types/react": {
|
"@types/react": {
|
||||||
|
@ -3542,12 +3614,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mui/utils": {
|
"node_modules/@mui/utils": {
|
||||||
"version": "5.14.10",
|
"version": "5.14.19",
|
||||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.10.tgz",
|
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.19.tgz",
|
||||||
"integrity": "sha512-Rn+vYQX7FxkcW0riDX/clNUwKuOJFH45HiULxwmpgnzQoQr3A0lb+QYwaZ+FAkZrR7qLoHKmLQlcItu6LT0y/Q==",
|
"integrity": "sha512-qAHvTXzk7basbyqPvhgWqN6JbmI2wLB/mf97GkSlz5c76MiKYV6Ffjvw9BjKZQ1YRb8rDX9kgdjRezOcoB91oQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.22.15",
|
"@babel/runtime": "^7.23.4",
|
||||||
"@types/prop-types": "^15.7.5",
|
"@types/prop-types": "^15.7.11",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react-is": "^18.2.0"
|
"react-is": "^18.2.0"
|
||||||
},
|
},
|
||||||
|
@ -3556,7 +3628,7 @@
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/mui"
|
"url": "https://opencollective.com/mui-org"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^17.0.0 || ^18.0.0",
|
"@types/react": "^17.0.0 || ^18.0.0",
|
||||||
|
@ -4227,9 +4299,9 @@
|
||||||
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
|
"integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.6",
|
"version": "15.7.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz",
|
||||||
"integrity": "sha512-RK/kBbYOQQHLYj9Z95eh7S6t7gq4Ojt/NT8HTk8bWVhA5DaF+5SMnxHKkP4gPNN3wAZkKP+VjAf0ebtYzf+fxg=="
|
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/q": {
|
"node_modules/@types/q": {
|
||||||
"version": "1.5.6",
|
"version": "1.5.6",
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
"@fontsource-variable/lexend": "^5.0.12",
|
"@fontsource-variable/lexend": "^5.0.12",
|
||||||
"@fontsource-variable/overpass": "^5.0.9",
|
"@fontsource-variable/overpass": "^5.0.9",
|
||||||
"@mui/icons-material": "^5.14.9",
|
"@mui/icons-material": "^5.14.9",
|
||||||
|
"@mui/lab": "^5.0.0-alpha.154",
|
||||||
"@mui/material": "^5.14.10",
|
"@mui/material": "^5.14.10",
|
||||||
"@types/node": "^16.18.52",
|
"@types/node": "^16.18.52",
|
||||||
"@types/react": "^18.2.22",
|
"@types/react": "^18.2.22",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, Divider } from "@mui/material"
|
import { Box, Divider } from "@mui/material"
|
||||||
import SidebarElement from "./SidebarElement"
|
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() {
|
function Sidebar() {
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ function Sidebar() {
|
||||||
|
|
||||||
<Box sx={{
|
<Box sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
@ -49,6 +50,11 @@ function Sidebar() {
|
||||||
<SidebarElement Title="Artikel" Icon={Newspaper} Path="/admin/artikel" />
|
<SidebarElement Title="Artikel" Icon={Newspaper} Path="/admin/artikel" />
|
||||||
<SidebarElement Title="Sponsoren" Icon={Savings} Path="/admin/sponsoren" />
|
<SidebarElement Title="Sponsoren" Icon={Savings} Path="/admin/sponsoren" />
|
||||||
<SidebarElement Title="Benutzer" Icon={Person} Path="/admin/benutzer" />
|
<SidebarElement Title="Benutzer" Icon={Person} Path="/admin/benutzer" />
|
||||||
|
|
||||||
|
<SidebarElement Title="Zur Website" Icon={ArrowBack} Path="/" sx={{
|
||||||
|
mt: "auto",
|
||||||
|
}} />
|
||||||
|
<SidebarElement Title="Logout" Icon={Logout} Path="/admin/logout" />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 { OverridableComponent } from "@mui/material/OverridableComponent";
|
||||||
import { useLocation, useNavigate } from "react-router-dom";
|
import { useLocation, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
@ -6,12 +6,14 @@ function SidebarElement({
|
||||||
Title,
|
Title,
|
||||||
Icon,
|
Icon,
|
||||||
Path,
|
Path,
|
||||||
|
sx
|
||||||
}: {
|
}: {
|
||||||
Title: string;
|
Title: string;
|
||||||
Icon: OverridableComponent<SvgIconTypeMap<{}, "svg">> & {
|
Icon: OverridableComponent<SvgIconTypeMap<{}, "svg">> & {
|
||||||
muiName: string;
|
muiName: string;
|
||||||
};
|
};
|
||||||
Path: string;
|
Path: string;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -38,6 +40,7 @@ function SidebarElement({
|
||||||
gap: "25px",
|
gap: "25px",
|
||||||
background: (theme) => theme.palette.primary.main,
|
background: (theme) => theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
|
...sx
|
||||||
}}
|
}}
|
||||||
onClick={() => navigate(Path)}
|
onClick={() => navigate(Path)}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,157 +0,0 @@
|
||||||
import { Avatar, Box, Typography } from "@mui/material";
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
|
|
||||||
function SponsorCard() {
|
|
||||||
const logoRef = useRef<HTMLInputElement>(null);
|
|
||||||
const bannerRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const [logo, setLogo] = useState<string>("");
|
|
||||||
const [banner, setBanner] = useState<string>("https://placehold.co/500x150");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={logoRef}
|
|
||||||
accept="image/*"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={(e) => {
|
|
||||||
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
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={bannerRef}
|
|
||||||
accept="image/*"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
onChange={(e) => {
|
|
||||||
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
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "500px",
|
|
||||||
height: "300px",
|
|
||||||
|
|
||||||
backgroundColor: "#fff",
|
|
||||||
|
|
||||||
// clip the edges using a clip path
|
|
||||||
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0% 100%)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={banner}
|
|
||||||
alt="banner"
|
|
||||||
style={{
|
|
||||||
width: "500px",
|
|
||||||
height: "150px",
|
|
||||||
|
|
||||||
objectFit: "cover",
|
|
||||||
minWidth: "500px",
|
|
||||||
minHeight: "150px",
|
|
||||||
|
|
||||||
cursor: "crosshair",
|
|
||||||
}}
|
|
||||||
onClick={() => bannerRef.current?.click()}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
mt: "-75px",
|
|
||||||
|
|
||||||
width: "500px",
|
|
||||||
height: "225px",
|
|
||||||
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "row",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
variant="square"
|
|
||||||
src={logo}
|
|
||||||
sx={{
|
|
||||||
width: "150px",
|
|
||||||
height: "150px",
|
|
||||||
|
|
||||||
borderRadius: "20px",
|
|
||||||
ml: "25px",
|
|
||||||
|
|
||||||
cursor: "crosshair",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
logoRef.current?.click();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
width: "300px",
|
|
||||||
height: "175px",
|
|
||||||
background: "#fff",
|
|
||||||
|
|
||||||
mt: "35px",
|
|
||||||
ml: "25px",
|
|
||||||
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
justifyContent: "flex-start",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
|
|
||||||
borderTopLeftRadius: "13px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography
|
|
||||||
contentEditable
|
|
||||||
sx={{
|
|
||||||
fontFamily: "Lexend Variable",
|
|
||||||
fontSize: "38px",
|
|
||||||
fontWeight: 800,
|
|
||||||
fontStyle: "italic",
|
|
||||||
ml: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Felix Orgel
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<Typography
|
|
||||||
contentEditable
|
|
||||||
sx={{
|
|
||||||
fontFamily: "Lexend Variable",
|
|
||||||
fontSize: "16px",
|
|
||||||
fontWeight: 200,
|
|
||||||
fontStyle: "italic",
|
|
||||||
color: "#828282",
|
|
||||||
ml: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
|
|
||||||
nonumy eirmod tempor invidunt u
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SponsorCard;
|
|
|
@ -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<HTMLInputElement>(null);
|
||||||
|
const bannerRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [logo, setLogo] = useState<string>("https://placehold.co/150x150");
|
||||||
|
const [banner, setBanner] = useState<string>("https://placehold.co/500x150");
|
||||||
|
|
||||||
|
const [name, setName] = useState<string>("");
|
||||||
|
const [description, setDescription] = useState<string>("");
|
||||||
|
|
||||||
|
const [url, setUrl] = useState<string>("");
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={logoRef}
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
ref={bannerRef}
|
||||||
|
accept="image/*"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onChange={(e) => {
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "500px",
|
||||||
|
height: "300px",
|
||||||
|
|
||||||
|
backgroundColor: "#FFF",
|
||||||
|
|
||||||
|
borderRadius: "13px",
|
||||||
|
border: "1px solid #00000033",
|
||||||
|
|
||||||
|
// clip the corners
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={banner}
|
||||||
|
alt="banner"
|
||||||
|
style={{
|
||||||
|
width: "500px",
|
||||||
|
height: "150px",
|
||||||
|
|
||||||
|
objectFit: "cover",
|
||||||
|
minWidth: "500px",
|
||||||
|
minHeight: "150px",
|
||||||
|
|
||||||
|
cursor: "crosshair",
|
||||||
|
}}
|
||||||
|
onClick={() => bannerRef.current?.click()}
|
||||||
|
/>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
mt: "-75px",
|
||||||
|
|
||||||
|
width: "500px",
|
||||||
|
height: "225px",
|
||||||
|
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
variant="square"
|
||||||
|
src={logo}
|
||||||
|
sx={{
|
||||||
|
width: "150px",
|
||||||
|
height: "150px",
|
||||||
|
|
||||||
|
borderRadius: "20px",
|
||||||
|
ml: "25px",
|
||||||
|
|
||||||
|
cursor: "crosshair",
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
logoRef.current?.click();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "300px",
|
||||||
|
height: "175px",
|
||||||
|
background: "#fff",
|
||||||
|
|
||||||
|
mt: "35px",
|
||||||
|
ml: "25px",
|
||||||
|
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
|
||||||
|
borderTopLeftRadius: "13px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
margin="none"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
value={name}
|
||||||
|
placeholder="Name"
|
||||||
|
sx={{
|
||||||
|
ml: "10px",
|
||||||
|
width: "280px",
|
||||||
|
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
fontFamily: "Lexend Variable",
|
||||||
|
fontSize: "38px",
|
||||||
|
fontWeight: 800,
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value.length >= 12) return;
|
||||||
|
if (e.target.value.includes("\n")) return;
|
||||||
|
setName(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
margin="none"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
value={description}
|
||||||
|
placeholder="Beschreibung"
|
||||||
|
sx={{
|
||||||
|
height: "90px",
|
||||||
|
minHeight: "90px",
|
||||||
|
width: "280px",
|
||||||
|
|
||||||
|
"& .MuiInputBase-root": {
|
||||||
|
display: "inline",
|
||||||
|
fontFamily: "Lexend Variable",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: 200,
|
||||||
|
fontStyle: "italic",
|
||||||
|
color: "#828282",
|
||||||
|
ml: "10px",
|
||||||
|
height: "90px",
|
||||||
|
minHeight: "90px",
|
||||||
|
textAlign: "left",
|
||||||
|
verticalAlign: "top",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value.length >= 100) return;
|
||||||
|
if (e.target.value.includes("\n")) return;
|
||||||
|
setDescription(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
margin="none"
|
||||||
|
variant="standard"
|
||||||
|
fullWidth
|
||||||
|
value={url}
|
||||||
|
placeholder="URL"
|
||||||
|
sx={{
|
||||||
|
mt: "20px",
|
||||||
|
width: "80%",
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value.length >= 128) return;
|
||||||
|
if (e.target.value.includes("\n")) return;
|
||||||
|
setUrl(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{ width: "80%" }}
|
||||||
|
disabled={!valid()}
|
||||||
|
onClick={() => {
|
||||||
|
onSave({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
logo,
|
||||||
|
banner,
|
||||||
|
url,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SponsorCardEditable;
|
|
@ -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 (
|
||||||
|
<Grid item>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={image}
|
||||||
|
alt=""
|
||||||
|
sx={{
|
||||||
|
height: "75px",
|
||||||
|
width: "75px",
|
||||||
|
borderRadius: "20px",
|
||||||
|
|
||||||
|
...(onRemove && {
|
||||||
|
cursor: "crosshair",
|
||||||
|
|
||||||
|
"&:hover": {
|
||||||
|
filter: "brightness(0.5)",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
...(!onRemove && {
|
||||||
|
cursor: "pointer",
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
onClick={
|
||||||
|
onRemove ||
|
||||||
|
(() => {
|
||||||
|
window.open(link, "_blank");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
|
@ -2,12 +2,14 @@ import { Close } from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
Backdrop,
|
Backdrop,
|
||||||
Box,
|
Box,
|
||||||
|
CircularProgress,
|
||||||
IconButton,
|
IconButton,
|
||||||
TextField,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import SponsorCard from "./SponsorCard";
|
import SponsorCardEditable from "./SponsorCardEditable";
|
||||||
|
import axios from "axios";
|
||||||
|
import { getBaseURL } from "../functions";
|
||||||
|
|
||||||
function SponsorModal({
|
function SponsorModal({
|
||||||
id,
|
id,
|
||||||
|
@ -16,8 +18,73 @@ function SponsorModal({
|
||||||
id: string | null;
|
id: string | null;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
const [data, setData] = useState<Types.Sponsor | null>(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 <></>;
|
if (!id) return <></>;
|
||||||
|
if (loading)
|
||||||
return (
|
return (
|
||||||
|
<Backdrop
|
||||||
|
open={true}
|
||||||
|
sx={{
|
||||||
|
zIndex: 2000,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: "Lexend Variable",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Lade...
|
||||||
|
</Typography>
|
||||||
|
</Backdrop>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{saving && (
|
||||||
|
<Backdrop
|
||||||
|
open={true}
|
||||||
|
sx={{
|
||||||
|
zIndex: 2000,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: "Lexend Variable",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Speichere...
|
||||||
|
</Typography>
|
||||||
|
</Backdrop>
|
||||||
|
)}
|
||||||
<Backdrop
|
<Backdrop
|
||||||
open={true}
|
open={true}
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -64,16 +131,79 @@ function SponsorModal({
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Sponsor Erstellen
|
Sponsor { id === "create" ? "Erstellen" : "Bearbeiten" }
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton onClick={onClose}>
|
<IconButton onClick={onClose}>
|
||||||
<Close />
|
<Close />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<SponsorCard />
|
<SponsorCardEditable
|
||||||
|
edit={(id && id !== "create" && {
|
||||||
|
id: id,
|
||||||
|
name: data?.name as string,
|
||||||
|
description: data?.description as string,
|
||||||
|
url: data?.url as string,
|
||||||
|
}) || undefined}
|
||||||
|
onSave={async (result) => {
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Backdrop>
|
</Backdrop>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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<boolean>(false);
|
||||||
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [username, setUsername] = useState<string>("");
|
||||||
|
const [password, setPassword] = useState<string>("");
|
||||||
|
|
||||||
|
const [permissions, setPermissions] = useState<Types.UserPermissions>({
|
||||||
|
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 (
|
||||||
|
<Backdrop
|
||||||
|
open={true}
|
||||||
|
sx={{
|
||||||
|
zIndex: 2000,
|
||||||
|
flexDirection: "column",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: "Lexend Variable",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Lade...
|
||||||
|
</Typography>
|
||||||
|
</Backdrop>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Backdrop
|
||||||
|
open={true}
|
||||||
|
sx={{
|
||||||
|
zIndex: 2000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "500px",
|
||||||
|
height: "auto",
|
||||||
|
padding: "10px",
|
||||||
|
|
||||||
|
backgroundColor: "#fff",
|
||||||
|
boxShadow: "0px 0px 10px #00000033",
|
||||||
|
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "auto",
|
||||||
|
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "40px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
fontFamily: "Lexend Variable",
|
||||||
|
fontSize: "28px",
|
||||||
|
fontWeight: 700,
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Benutzer {id === "create" ? "Erstellen" : "Bearbeiten"}
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={onClose}>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<TextField
|
||||||
|
label="Benutzername"
|
||||||
|
variant="outlined"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value.length >= 64) return;
|
||||||
|
if (e.target.value.includes("\n")) return;
|
||||||
|
setUsername(e.target.value);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: "80%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Passwort"
|
||||||
|
variant="outlined"
|
||||||
|
value={password}
|
||||||
|
type="password"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.value.length >= 64) return;
|
||||||
|
if (e.target.value.includes("\n")) return;
|
||||||
|
setPassword(e.target.value);
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: "80%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
|
||||||
|
width: "80%",
|
||||||
|
mx: "20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={permissions.admin}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPermissions({
|
||||||
|
...permissions,
|
||||||
|
admin: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Admin
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
disabled={permissions.admin}
|
||||||
|
checked={permissions.article_create}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPermissions({
|
||||||
|
...permissions,
|
||||||
|
article_create: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Artikel Erstellen
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
disabled={permissions.admin}
|
||||||
|
checked={permissions.article_manage}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPermissions({
|
||||||
|
...permissions,
|
||||||
|
article_manage: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Artikel Verwalten
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
disabled={permissions.admin}
|
||||||
|
checked={permissions.sponsor_manage}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPermissions({
|
||||||
|
...permissions,
|
||||||
|
sponsor_manage: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Sponsoren Verwalten
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
disabled={permissions.admin}
|
||||||
|
checked={permissions.user_manage}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPermissions({
|
||||||
|
...permissions,
|
||||||
|
user_manage: e.target.checked,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
Nutzer Verwalten
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<LoadingButton
|
||||||
|
variant="contained"
|
||||||
|
sx={{ width: "80%" }}
|
||||||
|
disabled={!valid()}
|
||||||
|
onClick={() => {
|
||||||
|
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
|
||||||
|
</LoadingButton>
|
||||||
|
</Box>
|
||||||
|
</Backdrop>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserModal;
|
|
@ -125,3 +125,6 @@ a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[name="suneditor_image_radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import Artikel from "./admin/Artikel";
|
||||||
import Sponsoren from "./admin/Sponsoren";
|
import Sponsoren from "./admin/Sponsoren";
|
||||||
import Benutzer from "./admin/Benutzer";
|
import Benutzer from "./admin/Benutzer";
|
||||||
import ArticleEditor from "./admin/ArticleEditor";
|
import ArticleEditor from "./admin/ArticleEditor";
|
||||||
|
import Logout from "./admin/Logout";
|
||||||
|
|
||||||
function AdminFrame() {
|
function AdminFrame() {
|
||||||
return (
|
return (
|
||||||
|
@ -34,6 +35,7 @@ function AdminFrame() {
|
||||||
<Route path="/benutzer" element={<Benutzer />} />
|
<Route path="/benutzer" element={<Benutzer />} />
|
||||||
<Route path="/editor" element={<ArticleEditor />} />
|
<Route path="/editor" element={<ArticleEditor />} />
|
||||||
<Route path="/editor/:id" element={<ArticleEditor />} />
|
<Route path="/editor/:id" element={<ArticleEditor />} />
|
||||||
|
<Route path="/logout" element={<Logout />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
/* eslint-disable jsx-a11y/alt-text */
|
/* eslint-disable jsx-a11y/alt-text */
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import TopBar from "../components/TopBar";
|
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 { getBaseURL } from "../functions";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
import "./Artikel.css";
|
import "./Artikel.css";
|
||||||
|
import "suneditor/dist/css/suneditor.min.css";
|
||||||
|
import { SponsorImageSmall } from "../components/SponsorImageSmall";
|
||||||
|
|
||||||
function Artikel() {
|
function Artikel() {
|
||||||
const ref = useRef<HTMLObjectElement>(null);
|
const ref = useRef<HTMLObjectElement>(null);
|
||||||
|
@ -16,25 +24,10 @@ function Artikel() {
|
||||||
|
|
||||||
const [loadingContent, setLoadingContent] = useState(true);
|
const [loadingContent, setLoadingContent] = useState(true);
|
||||||
const [article, setArticle] = useState<Types.Article | null>(null);
|
const [article, setArticle] = useState<Types.Article | null>(null);
|
||||||
const [banner, setBanner] = useState<string>("");
|
|
||||||
|
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
||||||
loadbanner();
|
|
||||||
|
|
||||||
axios.get(`${getBaseURL()}/api/article/content/${id}`).then((response) => {
|
axios.get(`${getBaseURL()}/api/article/content/${id}`).then((response) => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
ref.current.innerHTML = response.data;
|
ref.current.innerHTML = response.data;
|
||||||
|
@ -64,7 +57,8 @@ function Artikel() {
|
||||||
<TopBar />
|
<TopBar />
|
||||||
{loadingContent && <CircularProgress />}
|
{loadingContent && <CircularProgress />}
|
||||||
{!loadingContent && (
|
{!loadingContent && (
|
||||||
<Box sx={{
|
<Box
|
||||||
|
sx={{
|
||||||
height: "100vh",
|
height: "100vh",
|
||||||
width: "100vw",
|
width: "100vw",
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
|
@ -75,9 +69,10 @@ function Artikel() {
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src={banner}
|
src={`${getBaseURL()}/api/article/banner/${id}`}
|
||||||
alt=""
|
alt=""
|
||||||
style={{
|
style={{
|
||||||
height: "100vh",
|
height: "100vh",
|
||||||
|
@ -143,7 +138,26 @@ function Artikel() {
|
||||||
{article?.title}
|
{article?.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid container sx={{}}></Grid>
|
<Grid
|
||||||
|
container
|
||||||
|
spacing={2}
|
||||||
|
sx={{
|
||||||
|
pb: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{article?.sponsors.map((sponsor) => (
|
||||||
|
<Tooltip title={sponsor.name}>
|
||||||
|
<SponsorImageSmall
|
||||||
|
name={sponsor.name}
|
||||||
|
image={`${getBaseURL()}/api/sponsors/logo/${sponsor.ID}`}
|
||||||
|
description={sponsor.description}
|
||||||
|
link={sponsor.url}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<object
|
<object
|
||||||
id="article-content"
|
id="article-content"
|
||||||
|
|
|
@ -17,11 +17,16 @@ import {
|
||||||
Typography,
|
Typography,
|
||||||
Grid,
|
Grid,
|
||||||
Backdrop,
|
Backdrop,
|
||||||
|
Menu,
|
||||||
|
Autocomplete,
|
||||||
|
TextField,
|
||||||
|
Avatar,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { AddBox } from "@mui/icons-material";
|
import { AddBox } from "@mui/icons-material";
|
||||||
import { getBaseURL } from "../../functions";
|
import { getBaseURL } from "../../functions";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import { SponsorImageSmall } from "../../components/SponsorImageSmall";
|
||||||
|
|
||||||
function ArticleEditor() {
|
function ArticleEditor() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -44,25 +49,30 @@ function ArticleEditor() {
|
||||||
|
|
||||||
const [banner, setBanner] = useState<string>("");
|
const [banner, setBanner] = useState<string>("");
|
||||||
|
|
||||||
|
const [sponsorsAvail, setSponsorsAvail] = useState<Types.Sponsor[]>([]);
|
||||||
|
const [sponsorsSelected, setSponsorsSelected] = useState<Types.Sponsor[]>([]);
|
||||||
|
|
||||||
|
const sponsorsSelectedRef = useRef<Types.Sponsor[]>([]);
|
||||||
|
|
||||||
|
sponsorsSelectedRef.current = sponsorsSelected;
|
||||||
|
|
||||||
|
const [sponsorMenuEl, setSponsorMenuEl] = useState<null | HTMLElement>(null);
|
||||||
|
|
||||||
const loadbanner = () => {
|
const loadbanner = () => {
|
||||||
axios
|
if (!id) setBanner("https://placehold.co/1920x1080");
|
||||||
.get(`${getBaseURL()}/api/article/banner/${id}`)
|
else setBanner(`${getBaseURL()}/api/article/banner/${id}`);
|
||||||
.then((response) => {
|
|
||||||
setBanner(response.data);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
setBanner("https://placehold.co/1920x1080");
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async (content: string) => {
|
const save = async (content: string) => {
|
||||||
const title = document.getElementById("article-title")?.innerText;
|
const title = document.getElementById("article-title")?.innerText;
|
||||||
const banner = document.getElementById("articleBanner-content")?.getAttribute("src");
|
let banner = document
|
||||||
|
.getElementById("articleBanner-content")
|
||||||
|
?.getAttribute("src");
|
||||||
if (!title) return false;
|
if (!title) return false;
|
||||||
if (!banner) return false;
|
if (!banner) return false;
|
||||||
|
|
||||||
console.log(content);
|
if (banner.startsWith("http")) banner = "stale";
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const res = await axios
|
const res = await axios
|
||||||
.post(
|
.post(
|
||||||
|
@ -71,6 +81,7 @@ function ArticleEditor() {
|
||||||
: `${getBaseURL()}/api/article/edit/${id}`,
|
: `${getBaseURL()}/api/article/edit/${id}`,
|
||||||
{
|
{
|
||||||
title: title,
|
title: title,
|
||||||
|
sponsors: sponsorsSelectedRef.current.map((e) => e.ID),
|
||||||
content: content,
|
content: content,
|
||||||
image: banner,
|
image: banner,
|
||||||
},
|
},
|
||||||
|
@ -105,12 +116,12 @@ function ArticleEditor() {
|
||||||
if (!res.data) return;
|
if (!res.data) return;
|
||||||
|
|
||||||
document.getElementById("article-title")!.innerText = res.data.title;
|
document.getElementById("article-title")!.innerText = res.data.title;
|
||||||
|
setSponsorsSelected(res.data.sponsors);
|
||||||
|
|
||||||
axios.get(`${getBaseURL()}/api/article/content/${id}`).then((res) => {
|
axios.get(`${getBaseURL()}/api/article/content/${id}`).then((res) => {
|
||||||
if (!res.data) return;
|
if (!res.data) return;
|
||||||
|
|
||||||
editor.current?.setContents(res.data);
|
editor.current?.setContents(res.data);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// get all images with a data-size property
|
// get all images with a data-size property
|
||||||
const images = document.querySelectorAll("img[data-size]");
|
const images = document.querySelectorAll("img[data-size]");
|
||||||
|
@ -125,11 +136,61 @@ function ArticleEditor() {
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
axios.get(`${getBaseURL()}/api/sponsors`).then((res) => {
|
||||||
|
if (!res.data) return;
|
||||||
|
|
||||||
|
setSponsorsAvail(res.data);
|
||||||
|
});
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<Menu
|
||||||
|
anchorEl={sponsorMenuEl}
|
||||||
|
open={Boolean(sponsorMenuEl)}
|
||||||
|
onClose={() => {
|
||||||
|
setSponsorMenuEl(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Autocomplete
|
||||||
|
options={sponsorsAvail.filter(e => !sponsorsSelected.map(e => e.ID).includes(e.ID))}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
renderOption={(props, option) => (
|
||||||
|
<Box
|
||||||
|
component="li"
|
||||||
|
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
|
||||||
|
{...props}
|
||||||
|
onClick={() => {
|
||||||
|
sponsorsSelected.push(option as Types.Sponsor);
|
||||||
|
setSponsorsSelected(sponsorsSelected);
|
||||||
|
setSponsorMenuEl(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={`${getBaseURL()}/api/sponsors/logo/${option.ID}`}
|
||||||
|
sx={{
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
borderRadius: "20px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{option.name}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
renderInput={(params) => (
|
||||||
|
<TextField
|
||||||
|
{...params}
|
||||||
|
sx={{
|
||||||
|
width: "300px",
|
||||||
|
}}
|
||||||
|
fullWidth
|
||||||
|
label="Sponsoren"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Menu>
|
||||||
{loading && <CircularProgress />}
|
{loading && <CircularProgress />}
|
||||||
{!loading && (
|
{!loading && (
|
||||||
<Box
|
<Box
|
||||||
|
@ -158,7 +219,13 @@ function ArticleEditor() {
|
||||||
style={{
|
style={{
|
||||||
filter: "brightness(0.5)",
|
filter: "brightness(0.5)",
|
||||||
height: "100vh",
|
height: "100vh",
|
||||||
transform: "translateX(125px)",
|
width: "100vw",
|
||||||
|
position: "absolute",
|
||||||
|
top: "0px",
|
||||||
|
zIndex: -1,
|
||||||
|
objectFit: "cover",
|
||||||
|
minWidth: "100vw",
|
||||||
|
minHeight: "100vh",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -170,7 +237,7 @@ function ArticleEditor() {
|
||||||
accept="image/*"
|
accept="image/*"
|
||||||
style={{ display: "none" }}
|
style={{ display: "none" }}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const file = e.target.files?.[0]
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
@ -183,10 +250,11 @@ function ArticleEditor() {
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Box
|
||||||
<Box sx={{
|
sx={{
|
||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Backdrop
|
<Backdrop
|
||||||
open={saving}
|
open={saving}
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -268,7 +336,7 @@ function ArticleEditor() {
|
||||||
mb: "20px",
|
mb: "20px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Lorem ipsum whatever
|
Titel
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
|
@ -276,20 +344,23 @@ function ArticleEditor() {
|
||||||
spacing={2}
|
spacing={2}
|
||||||
sx={{
|
sx={{
|
||||||
pb: "20px",
|
pb: "20px",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "flex-start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{sponsorsSelected.map((sponsor) => (
|
||||||
<SponsorImageSmall
|
<SponsorImageSmall
|
||||||
name="Felix"
|
name={sponsor.name}
|
||||||
image="/logo.png"
|
image={`${getBaseURL()}/api/sponsors/logo/${sponsor.ID}`}
|
||||||
description="Felix ist ein cooler Typ"
|
description={sponsor.description}
|
||||||
link="#"
|
link={sponsor.url}
|
||||||
/>
|
onRemove={() => {
|
||||||
<SponsorImageSmall
|
setSponsorsSelected(
|
||||||
name="Felix"
|
sponsorsSelected.filter((e) => e.ID !== sponsor.ID)
|
||||||
image="/AdvanTex.jpg"
|
);
|
||||||
description="Felix ist ein cooler Typ"
|
}}
|
||||||
link="https://advantex.de/"
|
|
||||||
/>
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<AddBox
|
<AddBox
|
||||||
|
@ -303,7 +374,9 @@ function ArticleEditor() {
|
||||||
transform: "scale(1.1)",
|
transform: "scale(1.1)",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
onClick={() => {}}
|
onClick={(e) => {
|
||||||
|
setSponsorMenuEl(e.currentTarget as any);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -354,28 +427,4 @@ function ArticleEditor() {
|
||||||
|
|
||||||
export default ArticleEditor;
|
export default ArticleEditor;
|
||||||
|
|
||||||
function SponsorImageSmall({
|
|
||||||
name,
|
|
||||||
image,
|
|
||||||
description,
|
|
||||||
link,
|
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
image: string;
|
|
||||||
description: string;
|
|
||||||
link: string;
|
|
||||||
}): JSX.Element {
|
|
||||||
return (
|
|
||||||
<Grid item>
|
|
||||||
<Box sx={{}}>
|
|
||||||
<img
|
|
||||||
src={image}
|
|
||||||
alt=""
|
|
||||||
style={{
|
|
||||||
height: "25px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { ManageAccounts, PersonAdd, PersonRemove } from "@mui/icons-material";
|
||||||
ManageAccounts,
|
|
||||||
PersonAdd,
|
|
||||||
PersonRemove,
|
|
||||||
} from "@mui/icons-material";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
@ -17,17 +13,17 @@ import {
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { getBaseURL } from "../../functions";
|
import { getBaseURL } from "../../functions";
|
||||||
|
import UserModal from "../../components/UserModal";
|
||||||
|
|
||||||
function Benutzer() {
|
function Benutzer() {
|
||||||
const navigate = useNavigate();
|
const [modalID, setModelID] = useState<string | null>(null);
|
||||||
|
|
||||||
const [users, setUsers] = useState<Types.User[]>([]);
|
const [users, setUsers] = useState<Types.User[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
axios
|
axios
|
||||||
.get(`${getBaseURL()}/api/users/all`, {
|
.get(`${getBaseURL()}/api/users`, {
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||||
},
|
},
|
||||||
|
@ -41,6 +37,13 @@ function Benutzer() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<UserModal
|
||||||
|
id={modalID}
|
||||||
|
onClose={() => {
|
||||||
|
setModelID(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -84,10 +87,10 @@ function Benutzer() {
|
||||||
gap: "10px",
|
gap: "10px",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/admin/sponsor/");
|
setModelID("create");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PersonAdd /> Neue Benutzer
|
<PersonAdd /> Neuer Benutzer
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
@ -110,7 +113,7 @@ function Benutzer() {
|
||||||
Name
|
Name
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">Berechtigungen</TableCell>
|
<TableCell align="right">Berechtigungen</TableCell>
|
||||||
<TableCell align="right">Optionen</TableCell>
|
<TableCell align="right"></TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
@ -129,8 +132,7 @@ function Benutzer() {
|
||||||
.map((e) => e.name)
|
.map((e) => e.name)
|
||||||
.join(", ")}
|
.join(", ")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" width="200px"></TableCell>
|
<TableCell align="right">
|
||||||
<TableCell align="right" width="60px">
|
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -149,6 +151,9 @@ function Benutzer() {
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
}}
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setModelID(user.ID);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ManageAccounts />
|
<ManageAccounts />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -165,6 +170,21 @@ function Benutzer() {
|
||||||
background: "red",
|
background: "red",
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
axios
|
||||||
|
.delete(`${getBaseURL()}/api/users/delete/${user.ID}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${localStorage.getItem(
|
||||||
|
"token"
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
setUsers(
|
||||||
|
users.filter((e) => e.ID !== user.ID)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PersonRemove />
|
<PersonRemove />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -175,6 +195,7 @@ function Benutzer() {
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,34 @@
|
||||||
import { Visibility, Newspaper, Person, Savings } from "@mui/icons-material";
|
import { Visibility, Newspaper, Person, Savings } from "@mui/icons-material";
|
||||||
import { Box, Grid, SvgIconTypeMap, Typography } from "@mui/material";
|
import { Box, Grid, SvgIconTypeMap, Typography } from "@mui/material";
|
||||||
import { OverridableComponent } from "@mui/material/OverridableComponent";
|
import { OverridableComponent } from "@mui/material/OverridableComponent";
|
||||||
|
import axios from "axios";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { getBaseURL } from "../../functions";
|
||||||
|
|
||||||
function Dashboard() {
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -19,10 +45,10 @@ function Dashboard() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<StatDisplay Title="Benutzer" Icon={Person} Value="0" />
|
<StatDisplay Title="Benutzer" Icon={Person} Value={stats.users.toFixed(0)} />
|
||||||
<StatDisplay Title="Artikel" Icon={Newspaper} Value="0" />
|
<StatDisplay Title="Artikel" Icon={Newspaper} Value={stats.articles.toFixed(0)} />
|
||||||
<StatDisplay Title="Sponsoren" Icon={Savings} Value="0" />
|
<StatDisplay Title="Sponsoren" Icon={Savings} Value={stats.sponsors.toFixed(0)} />
|
||||||
<StatDisplay Title="Klicks" Icon={Visibility} Value="0" />
|
<StatDisplay Title="Views" Icon={Visibility} Value={stats.views.toFixed(0)} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
@ -49,7 +75,8 @@ function StatDisplay({
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|
||||||
padding: "10px",
|
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%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
|
@ -74,14 +101,16 @@ function StatDisplay({
|
||||||
{Title}
|
{Title}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
<Box sx={{
|
<Box
|
||||||
|
sx={{
|
||||||
width: "50px",
|
width: "50px",
|
||||||
height: "50px",
|
height: "50px",
|
||||||
|
|
||||||
padding: "5px",
|
padding: "5px",
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
sx={{
|
sx={{
|
||||||
fontSize: "40px",
|
fontSize: "40px",
|
||||||
|
@ -90,17 +119,21 @@ function StatDisplay({
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{
|
<Box
|
||||||
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
}}>
|
}}
|
||||||
<Typography sx={{
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
fontFamily: "Lexend Variable",
|
fontFamily: "Lexend Variable",
|
||||||
fontSize: "40px",
|
fontSize: "40px",
|
||||||
color: "#000000FF",
|
color: "#000000FF",
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
{Value}
|
{Value}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -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 (
|
||||||
|
<Backdrop open={true} sx={{
|
||||||
|
zIndex: 1000,
|
||||||
|
flexDirection: "column",
|
||||||
|
display: "flex",
|
||||||
|
}}>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography sx={{
|
||||||
|
fontFamily: "Lexend Variable",
|
||||||
|
fontSize: "20px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
color: "#fff",
|
||||||
|
}}>
|
||||||
|
Logge aus...
|
||||||
|
</Typography>
|
||||||
|
</Backdrop>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Logout
|
|
@ -15,21 +15,51 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
Typography,
|
Typography,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Avatar,
|
||||||
|
Backdrop,
|
||||||
|
CircularProgress,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import SponsorModal from "../../components/SponsorModal";
|
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() {
|
function Sponsoren() {
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const [modalID, setModelID] = useState<string | null>(null);
|
const [modalID, setModelID] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [sponsors, setSponsors] = useState<Types.Sponsor[]>([]);
|
||||||
|
const [loaded, setLoaded] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoaded(false);
|
||||||
|
axios.get(`${getBaseURL()}/api/sponsors/`).then((res) => {
|
||||||
|
setSponsors(res.data);
|
||||||
|
setLoaded(true);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SponsorModal id={modalID} onClose={() => {
|
<Backdrop
|
||||||
|
open={!loaded}
|
||||||
|
sx={{
|
||||||
|
zIndex: 2000,
|
||||||
|
color: "#fff",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress
|
||||||
|
sx={{
|
||||||
|
color: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Backdrop>
|
||||||
|
<SponsorModal
|
||||||
|
id={modalID}
|
||||||
|
onClose={() => {
|
||||||
setModelID(null);
|
setModelID(null);
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -95,20 +125,33 @@ function Sponsoren() {
|
||||||
>
|
>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell align="left" width="700px">
|
<TableCell align="left" width="50px">
|
||||||
|
{" "}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="left" width="100%">
|
||||||
Name
|
Name
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right">Ansprechpartner</TableCell>
|
|
||||||
<TableCell align="right">Datum</TableCell>
|
<TableCell align="right">Datum</TableCell>
|
||||||
<TableCell align="right">Optionen</TableCell>
|
<TableCell align="right">Optionen</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
<TableRow>
|
{sponsors.map((sponsor) => (
|
||||||
<TableCell align="left">RF Computer GMBH</TableCell>
|
<TableRow key={sponsor.ID}>
|
||||||
<TableCell align="right">R. Fink</TableCell>
|
<TableCell align="left" width="50px" padding="none">
|
||||||
|
<Avatar
|
||||||
|
src={`${getBaseURL()}/api/sponsors/logo/${sponsor.ID}`}
|
||||||
|
alt="logo"
|
||||||
|
style={{
|
||||||
|
width: "50px",
|
||||||
|
height: "50px",
|
||||||
|
borderRadius: "20px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell align="left">{sponsor.name}</TableCell>
|
||||||
<TableCell align="right" width="200px">
|
<TableCell align="right" width="200px">
|
||||||
7 Nov. 2023
|
{moment(sponsor.addedAt).locale("de").format("ll")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" width="60px">
|
<TableCell align="right" width="60px">
|
||||||
<Box
|
<Box
|
||||||
|
@ -130,6 +173,9 @@ function Sponsoren() {
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
borderRadius: "5px",
|
borderRadius: "5px",
|
||||||
}}
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
setModelID(sponsor.ID);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<ManageAccounts />
|
<ManageAccounts />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -148,6 +194,26 @@ function Sponsoren() {
|
||||||
background: "red",
|
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)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DomainDisabledRounded />
|
<DomainDisabledRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -155,6 +221,7 @@ function Sponsoren() {
|
||||||
</Box>
|
</Box>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
))}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -7,22 +7,28 @@ declare namespace Types {
|
||||||
ID: string;
|
ID: string;
|
||||||
username: string;
|
username: string;
|
||||||
}
|
}
|
||||||
sponsors: {
|
sponsors: Sponsor[];
|
||||||
ID: string;
|
|
||||||
name: string;
|
|
||||||
url: string;
|
|
||||||
description: string;
|
|
||||||
}[];
|
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}
|
}
|
||||||
interface User {
|
interface User extends UserPermissions {
|
||||||
ID: string;
|
ID: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserPermissions {
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
article_create: boolean;
|
article_create: boolean;
|
||||||
article_manage: boolean;
|
article_manage: boolean;
|
||||||
sponsor_manage: boolean;
|
sponsor_manage: boolean;
|
||||||
user_manage: boolean;
|
user_manage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Sponsor {
|
||||||
|
ID: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
description: string;
|
||||||
|
addedAt: Date;
|
||||||
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue