Built a fckn cdn

This commit is contained in:
Ipmake 2023-12-05 00:00:08 +01:00
parent 543aca02ba
commit 5ce485e924
19 changed files with 1769 additions and 589 deletions

View File

@ -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}`)
}) })

View File

@ -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",

View File

@ -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",

View File

@ -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>
) )

View File

@ -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)}
> >

View File

@ -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;

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
</>
); );
} }

View File

@ -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;

View File

@ -125,3 +125,6 @@ a {
text-decoration: none; text-decoration: none;
} }
input[name="suneditor_image_radio"] {
display: none;
}

View File

@ -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>

View File

@ -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"

View File

@ -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>
);
}

View File

@ -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>
</>
); );
} }

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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;
}
} }