feat: v3.2.0 - custom themes & curated themes
This commit is contained in:
parent
47db6cf1bd
commit
c5cef56e2a
17 changed files with 372 additions and 33 deletions
|
@ -17,9 +17,8 @@ Fast & lightweight file uploading.
|
||||||
- Configurable
|
- Configurable
|
||||||
- Fast
|
- Fast
|
||||||
- Built with Next.js & React
|
- Built with Next.js & React
|
||||||
- Support for **multible database types**
|
|
||||||
- Token protected uploading
|
- Token protected uploading
|
||||||
- Easy setup instructions on [docs](https://zipline.diced.me)
|
- Easy setup instructions on [docs](https://zipline.diced.me) (One command install `docker-compose up`)
|
||||||
|
|
||||||
# Installing
|
# Installing
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "zip3",
|
"name": "zip3",
|
||||||
"version": "3.1.0",
|
"version": "3.2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"dev": "NODE_ENV=development node server",
|
"dev": "NODE_ENV=development node server",
|
||||||
|
|
25
prisma/migrations/20210826034827_custom_themes/migration.sql
Normal file
25
prisma/migrations/20210826034827_custom_themes/migration.sql
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "systemTheme" TEXT NOT NULL DEFAULT E'dark_blue';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Theme" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"type" TEXT NOT NULL,
|
||||||
|
"primary" TEXT NOT NULL,
|
||||||
|
"secondary" TEXT NOT NULL,
|
||||||
|
"error" TEXT NOT NULL,
|
||||||
|
"warning" TEXT NOT NULL,
|
||||||
|
"info" TEXT NOT NULL,
|
||||||
|
"border" TEXT NOT NULL,
|
||||||
|
"mainBackground" TEXT NOT NULL,
|
||||||
|
"paperBackground" TEXT NOT NULL,
|
||||||
|
"userId" INTEGER NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Theme_userId_unique" ON "Theme"("userId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Theme" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -13,12 +13,29 @@ model User {
|
||||||
password String
|
password String
|
||||||
token String
|
token String
|
||||||
administrator Boolean @default(false)
|
administrator Boolean @default(false)
|
||||||
|
systemTheme String @default("dark_blue")
|
||||||
|
customTheme Theme?
|
||||||
embedTitle String?
|
embedTitle String?
|
||||||
embedColor String @default("#2f3136")
|
embedColor String @default("#2f3136")
|
||||||
images Image[]
|
images Image[]
|
||||||
urls Url[]
|
urls Url[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Theme {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
type String
|
||||||
|
primary String
|
||||||
|
secondary String
|
||||||
|
error String
|
||||||
|
warning String
|
||||||
|
info String
|
||||||
|
border String
|
||||||
|
mainBackground String
|
||||||
|
paperBackground String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
userId Int
|
||||||
|
}
|
||||||
|
|
||||||
model Image {
|
model Image {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
file String
|
file String
|
||||||
|
@ -31,25 +48,25 @@ model Image {
|
||||||
}
|
}
|
||||||
|
|
||||||
model InvisibleImage {
|
model InvisibleImage {
|
||||||
id Int
|
id Int
|
||||||
image Image @relation(fields: [id], references: [id])
|
image Image @relation(fields: [id], references: [id])
|
||||||
|
|
||||||
invis String @unique
|
invis String @unique
|
||||||
}
|
}
|
||||||
|
|
||||||
model Url {
|
model Url {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
to String
|
to String
|
||||||
created_at DateTime @default(now())
|
created_at DateTime @default(now())
|
||||||
views Int @default(0)
|
views Int @default(0)
|
||||||
invisible InvisibleUrl?
|
invisible InvisibleUrl?
|
||||||
user User @relation(fields: [userId], references: [id])
|
user User @relation(fields: [userId], references: [id])
|
||||||
userId Int
|
userId Int
|
||||||
}
|
}
|
||||||
|
|
||||||
model InvisibleUrl {
|
model InvisibleUrl {
|
||||||
id Int
|
id Int
|
||||||
url Url @relation(fields: [id], references: [id])
|
url Url @relation(fields: [id], references: [id])
|
||||||
|
|
||||||
invis String @unique
|
invis String @unique
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,8 @@ const prismaRun = require('./prisma-run');
|
||||||
|
|
||||||
module.exports = async (config) => {
|
module.exports = async (config) => {
|
||||||
try {
|
try {
|
||||||
await prismaRun(config.database.url, ['migrate', 'deploy', '--schema=prisma/schema.prisma']);
|
await prismaRun(config.database.url, ['migrate', 'deploy']);
|
||||||
await prismaRun(config.database.url, ['generate', '--schema=prisma/schema.prisma']);
|
await prismaRun(config.database.url, ['generate']);
|
||||||
await prismaRun(config.database.url, ['db', 'seed', '--preview-feature', '--schema=prisma/schema.prisma']);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
Logger.get('db').error('there was an error.. exiting..');
|
Logger.get('db').error('there was an error.. exiting..');
|
||||||
|
|
|
@ -23,6 +23,7 @@ import {
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
|
Select,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import {
|
import {
|
||||||
Menu as MenuIcon,
|
Menu as MenuIcon,
|
||||||
|
@ -33,10 +34,15 @@ import {
|
||||||
ContentCopy as CopyIcon,
|
ContentCopy as CopyIcon,
|
||||||
Autorenew as ResetIcon,
|
Autorenew as ResetIcon,
|
||||||
Logout as LogoutIcon,
|
Logout as LogoutIcon,
|
||||||
PeopleAlt as UsersIcon
|
PeopleAlt as UsersIcon,
|
||||||
|
Brush as BrushIcon
|
||||||
} from '@material-ui/icons';
|
} from '@material-ui/icons';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import Backdrop from './Backdrop';
|
import Backdrop from './Backdrop';
|
||||||
|
import { friendlyThemeName, themes } from './Theming';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import { useStoreDispatch } from 'lib/redux/store';
|
||||||
|
import { updateUser } from 'lib/redux/reducers/user';
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
|
@ -122,11 +128,14 @@ function ResetTokenDialog({ open, setOpen, setToken }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Layout({ children, user, loading, noPaper }) {
|
export default function Layout({ children, user, loading, noPaper }) {
|
||||||
|
const [systemTheme, setSystemTheme] = useState(user.systemTheme || 'dark_blue');
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
const [anchorEl, setAnchorEl] = useState(null);
|
||||||
const [copyOpen, setCopyOpen] = useState(false);
|
const [copyOpen, setCopyOpen] = useState(false);
|
||||||
const [resetOpen, setResetOpen] = useState(false);
|
const [resetOpen, setResetOpen] = useState(false);
|
||||||
const [token, setToken] = useState(user?.token);
|
const [token, setToken] = useState(user?.token);
|
||||||
|
const router = useRouter();
|
||||||
|
const dispatch = useStoreDispatch();
|
||||||
|
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const handleClick = e => setAnchorEl(e.currentTarget);
|
const handleClick = e => setAnchorEl(e.currentTarget);
|
||||||
|
@ -142,6 +151,17 @@ export default function Layout({ children, user, loading, noPaper }) {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateTheme = async (event: React.ChangeEvent<{ value: string }>) => {
|
||||||
|
const newUser = await useFetch('/api/user', 'PATCH', {
|
||||||
|
systemTheme: event.target.value || 'dark_blue'
|
||||||
|
});
|
||||||
|
|
||||||
|
setSystemTheme(newUser.systemTheme);
|
||||||
|
dispatch(updateUser(newUser));
|
||||||
|
|
||||||
|
router.replace(router.pathname);
|
||||||
|
};
|
||||||
|
|
||||||
const drawer = (
|
const drawer = (
|
||||||
<div>
|
<div>
|
||||||
<CopyTokenDialog open={copyOpen} setOpen={setCopyOpen} token={token} />
|
<CopyTokenDialog open={copyOpen} setOpen={setCopyOpen} token={token} />
|
||||||
|
@ -221,6 +241,22 @@ export default function Layout({ children, user, loading, noPaper }) {
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
|
<MenuItem>
|
||||||
|
<BrushIcon sx={{ mr: 2 }} />
|
||||||
|
<Select
|
||||||
|
variant='standard'
|
||||||
|
label='Theme'
|
||||||
|
value={systemTheme}
|
||||||
|
onChange={handleUpdateTheme}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{Object.keys(themes).map(t => (
|
||||||
|
<MenuItem value={t} key={t}>
|
||||||
|
{friendlyThemeName[t]}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
53
src/components/Theming.tsx
Normal file
53
src/components/Theming.tsx
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ThemeProvider } from '@emotion/react';
|
||||||
|
import { CssBaseline } from '@material-ui/core';
|
||||||
|
import dark_blue from 'lib/themes/dark_blue';
|
||||||
|
import dark from 'lib/themes/dark';
|
||||||
|
import ayu_dark from 'lib/themes/ayu_dark';
|
||||||
|
import { useStoreSelector } from 'lib/redux/store';
|
||||||
|
import createTheme from 'lib/themes';
|
||||||
|
|
||||||
|
export const themes = {
|
||||||
|
'dark_blue': dark_blue,
|
||||||
|
'dark': dark,
|
||||||
|
'ayu_dark': ayu_dark
|
||||||
|
};
|
||||||
|
|
||||||
|
export const friendlyThemeName = {
|
||||||
|
'dark_blue': 'Dark Blue',
|
||||||
|
'dark': 'Very Dark',
|
||||||
|
'ayu_dark': 'Ayu Dark'
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ZiplineTheming({ Component, pageProps }) {
|
||||||
|
let t;
|
||||||
|
|
||||||
|
const user = useStoreSelector(state => state.user);
|
||||||
|
if (!user) t = themes.dark_blue;
|
||||||
|
else {
|
||||||
|
if (user.customTheme) {
|
||||||
|
t = createTheme({
|
||||||
|
type: 'dark',
|
||||||
|
primary: user.customTheme.primary,
|
||||||
|
secondary: user.customTheme.secondary,
|
||||||
|
error: user.customTheme.error,
|
||||||
|
warning: user.customTheme.warning,
|
||||||
|
info: user.customTheme.info,
|
||||||
|
border: user.customTheme.border,
|
||||||
|
background: {
|
||||||
|
main: user.customTheme.mainBackground,
|
||||||
|
paper: user.customTheme.paperBackground
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
t = themes[user.systemTheme] ?? themes.dark_blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={t}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -24,6 +24,7 @@ type Aligns = 'inherit' | 'right' | 'left' | 'center' | 'justify';
|
||||||
|
|
||||||
export function bytesToRead(bytes: number) {
|
export function bytesToRead(bytes: number) {
|
||||||
if (isNaN(bytes)) return '0.0 B';
|
if (isNaN(bytes)) return '0.0 B';
|
||||||
|
if (bytes === Infinity) return '0.0 B';
|
||||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
let num = 0;
|
let num = 0;
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@ export default function Dashboard() {
|
||||||
const imgs = await useFetch('/api/user/images');
|
const imgs = await useFetch('/api/user/images');
|
||||||
const stts = await useFetch('/api/stats');
|
const stts = await useFetch('/api/stats');
|
||||||
setImages(imgs);
|
setImages(imgs);
|
||||||
setStats(stts);
|
setStats(stts);console.log(stts);
|
||||||
|
|
||||||
setApiLoading(false);
|
setApiLoading(false);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { TextField, Button, Box, Typography } from '@material-ui/core';
|
import { TextField, Button, Box, Typography, Select, MenuItem } from '@material-ui/core';
|
||||||
|
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
import * as yup from 'yup';
|
import * as yup from 'yup';
|
||||||
|
@ -8,6 +8,7 @@ import Backdrop from 'components/Backdrop';
|
||||||
import Alert from 'components/Alert';
|
import Alert from 'components/Alert';
|
||||||
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
||||||
import { updateUser } from 'lib/redux/reducers/user';
|
import { updateUser } from 'lib/redux/reducers/user';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
const validationSchema = yup.object({
|
const validationSchema = yup.object({
|
||||||
username: yup
|
username: yup
|
||||||
|
@ -15,6 +16,45 @@ const validationSchema = yup.object({
|
||||||
.required('Username is required')
|
.required('Username is required')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const themeValidationSchema = yup.object({
|
||||||
|
type: yup
|
||||||
|
.string()
|
||||||
|
.required('Type (dark, light) is required is required'),
|
||||||
|
primary: yup
|
||||||
|
.string()
|
||||||
|
.required('Primary color is required')
|
||||||
|
.matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }),
|
||||||
|
secondary: yup
|
||||||
|
.string()
|
||||||
|
.required('Secondary color is required')
|
||||||
|
.matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }),
|
||||||
|
error: yup
|
||||||
|
.string()
|
||||||
|
.required('Error color is required')
|
||||||
|
.matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }),
|
||||||
|
warning: yup
|
||||||
|
.string()
|
||||||
|
.required('Warning color is required')
|
||||||
|
.matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }),
|
||||||
|
info: yup
|
||||||
|
.string()
|
||||||
|
.required('Info color is required')
|
||||||
|
.matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }),
|
||||||
|
border: yup
|
||||||
|
.string()
|
||||||
|
.required('Border color is required')
|
||||||
|
.matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }),
|
||||||
|
mainBackground: yup
|
||||||
|
.string()
|
||||||
|
.required('Main Background is required')
|
||||||
|
.matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }),
|
||||||
|
paperBackground: yup
|
||||||
|
.string()
|
||||||
|
.required('Paper Background is required')
|
||||||
|
.matches(/\#[0-9A-Fa-f]{6}/g, { message: 'Not a valid hex color' }),
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
function TextInput({ id, label, formik, ...other }) {
|
function TextInput({ id, label, formik, ...other }) {
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
|
@ -36,6 +76,7 @@ function TextInput({ id, label, formik, ...other }) {
|
||||||
export default function Manage() {
|
export default function Manage() {
|
||||||
const user = useStoreSelector(state => state.user);
|
const user = useStoreSelector(state => state.user);
|
||||||
const dispatch = useStoreDispatch();
|
const dispatch = useStoreDispatch();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
@ -84,6 +125,41 @@ export default function Manage() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customThemeFormik = useFormik({
|
||||||
|
initialValues: {
|
||||||
|
type: user.customTheme?.type || 'dark',
|
||||||
|
primary: user.customTheme?.primary || '',
|
||||||
|
secondary: user.customTheme?.secondary || '',
|
||||||
|
error: user.customTheme?.error || '',
|
||||||
|
warning: user.customTheme?.warning || '',
|
||||||
|
info: user.customTheme?.info || '',
|
||||||
|
border: user.customTheme?.border || '',
|
||||||
|
mainBackground: user.customTheme?.mainBackground || '',
|
||||||
|
paperBackground: user.customTheme?.paperBackground || '',
|
||||||
|
},
|
||||||
|
validationSchema: themeValidationSchema,
|
||||||
|
onSubmit: async values => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const newUser = await useFetch('/api/user', 'PATCH', { customTheme: values });
|
||||||
|
console.log(newUser);
|
||||||
|
|
||||||
|
if (newUser.error) {
|
||||||
|
setLoading(false);
|
||||||
|
setMessage('An error occured');
|
||||||
|
setSeverity('error');
|
||||||
|
setOpen(true);
|
||||||
|
} else {
|
||||||
|
dispatch(updateUser(newUser));
|
||||||
|
router.replace(router.pathname);
|
||||||
|
setLoading(false);
|
||||||
|
setMessage('Saved theme');
|
||||||
|
setSeverity('success');
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Backdrop open={loading}/>
|
<Backdrop open={loading}/>
|
||||||
|
@ -104,7 +180,42 @@ export default function Manage() {
|
||||||
<Button
|
<Button
|
||||||
variant='contained'
|
variant='contained'
|
||||||
type='submit'
|
type='submit'
|
||||||
>Save</Button>
|
>Save User</Button>
|
||||||
|
</Box>
|
||||||
|
</form>
|
||||||
|
<Typography variant='h4' py={2}>Manage Theme</Typography>
|
||||||
|
<form onSubmit={customThemeFormik.handleSubmit}>
|
||||||
|
<Select
|
||||||
|
id='type'
|
||||||
|
name='type'
|
||||||
|
label='Type'
|
||||||
|
value={customThemeFormik.values['type']}
|
||||||
|
onChange={customThemeFormik.handleChange}
|
||||||
|
error={customThemeFormik.touched['type'] && Boolean(customThemeFormik.errors['type'])}
|
||||||
|
variant='standard'
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<MenuItem value='dark'>Dark Theme</MenuItem>
|
||||||
|
<MenuItem value='light'>Light Theme</MenuItem>
|
||||||
|
</Select>
|
||||||
|
<TextInput id='primary' label='Primary Color' formik={customThemeFormik} />
|
||||||
|
<TextInput id='secondary' label='Secondary Color' formik={customThemeFormik} />
|
||||||
|
<TextInput id='error' label='Error Color' formik={customThemeFormik} />
|
||||||
|
<TextInput id='warning' label='Warning Color' formik={customThemeFormik} />
|
||||||
|
<TextInput id='info' label='Info Color' formik={customThemeFormik} />
|
||||||
|
<TextInput id='border' label='Border Color' formik={customThemeFormik} />
|
||||||
|
<TextInput id='mainBackground' label='Main Background' formik={customThemeFormik} />
|
||||||
|
<TextInput id='paperBackground' label='Paper Background' formik={customThemeFormik} />
|
||||||
|
<Box
|
||||||
|
display='flex'
|
||||||
|
justifyContent='right'
|
||||||
|
alignItems='right'
|
||||||
|
pt={2}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant='contained'
|
||||||
|
type='submit'
|
||||||
|
>Save Theme</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</form>
|
</form>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import type { CookieSerializeOptions } from 'cookie';
|
import type { CookieSerializeOptions } from 'cookie';
|
||||||
import type { User } from '@prisma/client';
|
import type { Image, Theme, User } from '@prisma/client';
|
||||||
|
|
||||||
import { serialize } from 'cookie';
|
import { serialize } from 'cookie';
|
||||||
import { sign64, unsign64 } from '../util';
|
import { sign64, unsign64 } from '../util';
|
||||||
|
@ -17,7 +17,18 @@ export interface NextApiFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NextApiReq = NextApiRequest & {
|
export type NextApiReq = NextApiRequest & {
|
||||||
user: () => Promise<User | null | void>;
|
user: () => Promise<{
|
||||||
|
username: string;
|
||||||
|
token: string;
|
||||||
|
embedTitle: string;
|
||||||
|
embedColor: string;
|
||||||
|
systemTheme: string;
|
||||||
|
customTheme?: Theme;
|
||||||
|
administrator: boolean;
|
||||||
|
id: number;
|
||||||
|
images: Image[];
|
||||||
|
password: string;
|
||||||
|
} | null | void>;
|
||||||
getCookie: (name: string) => string | null;
|
getCookie: (name: string) => string | null;
|
||||||
cleanCookie: (name: string) => void;
|
cleanCookie: (name: string) => void;
|
||||||
file?: NextApiFile;
|
file?: NextApiFile;
|
||||||
|
@ -83,6 +94,18 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: Number(userId)
|
id: Number(userId)
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
administrator: true,
|
||||||
|
embedColor: true,
|
||||||
|
embedTitle: true,
|
||||||
|
id: true,
|
||||||
|
images: true,
|
||||||
|
password: true,
|
||||||
|
systemTheme: true,
|
||||||
|
customTheme: true,
|
||||||
|
token: true,
|
||||||
|
username: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Theme } from '@prisma/client';
|
||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
|
@ -5,6 +6,8 @@ export interface User {
|
||||||
token: string;
|
token: string;
|
||||||
embedTitle: string;
|
embedTitle: string;
|
||||||
embedColor: string;
|
embedColor: string;
|
||||||
|
systemTheme: string;
|
||||||
|
customTheme?: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: User = null;
|
const initialState: User = null;
|
||||||
|
|
17
src/lib/themes/ayu_dark.ts
Normal file
17
src/lib/themes/ayu_dark.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
// https://github.com/AlphaNecron/
|
||||||
|
// https://github.com/ayu-theme/ayu-colors
|
||||||
|
import createTheme from '.';
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
type: 'dark',
|
||||||
|
primary: '#E6B450',
|
||||||
|
secondary: '#FFEE99',
|
||||||
|
error: '#FF3333',
|
||||||
|
warning: '#F29668',
|
||||||
|
info: '#95E6CB',
|
||||||
|
border: '#0A0E14',
|
||||||
|
background: {
|
||||||
|
main: '#0D1016',
|
||||||
|
paper: '#0A0E14'
|
||||||
|
}
|
||||||
|
});
|
15
src/lib/themes/dark.ts
Normal file
15
src/lib/themes/dark.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import createTheme from '.';
|
||||||
|
|
||||||
|
export default createTheme({
|
||||||
|
type: 'dark',
|
||||||
|
primary: '#2c39a6',
|
||||||
|
secondary: '#7344e2',
|
||||||
|
error: '#ff4141',
|
||||||
|
warning: '#ff9800',
|
||||||
|
info: '#2f6fb9',
|
||||||
|
border: '#2b2b2b',
|
||||||
|
background: {
|
||||||
|
main: '#000000',
|
||||||
|
paper: '#060606'
|
||||||
|
}
|
||||||
|
});
|
|
@ -2,9 +2,7 @@ import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { ThemeProvider } from '@material-ui/core/styles';
|
import Theming from 'components/Theming';
|
||||||
import CssBaseline from '@material-ui/core/CssBaseline';
|
|
||||||
import theme from 'lib/themes/dark_blue';
|
|
||||||
import { useStore } from 'lib/redux/store';
|
import { useStore } from 'lib/redux/store';
|
||||||
|
|
||||||
export default function MyApp({ Component, pageProps }) {
|
export default function MyApp({ Component, pageProps }) {
|
||||||
|
@ -22,10 +20,10 @@ export default function MyApp({ Component, pageProps }) {
|
||||||
<meta name='description' content='Zipline' />
|
<meta name='description' content='Zipline' />
|
||||||
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
|
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
|
||||||
</Head>
|
</Head>
|
||||||
<ThemeProvider theme={theme}>
|
<Theming
|
||||||
<CssBaseline />
|
Component={Component}
|
||||||
<Component {...pageProps} />
|
pageProps={pageProps}
|
||||||
</ThemeProvider>
|
/>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,18 @@ import prisma from 'lib/prisma';
|
||||||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||||
import { checkPassword } from 'lib/util';
|
import { checkPassword } from 'lib/util';
|
||||||
import Logger from 'lib/logger';
|
import Logger from 'lib/logger';
|
||||||
|
import prismaRun from '../../../../scripts/prisma-run';
|
||||||
|
import config from 'lib/config';
|
||||||
|
|
||||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
if (req.method !== 'POST') return res.status(405).end();
|
if (req.method !== 'POST') return res.status(405).end();
|
||||||
const { username, password } = req.body as { username: string, password: string };
|
const { username, password } = req.body as { username: string, password: string };
|
||||||
|
|
||||||
|
const users = await prisma.user.findMany();
|
||||||
|
if (users.length === 0) {
|
||||||
|
await prismaRun(config.database.url, ['db', 'seed', '--preview-feature']);
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
username
|
username
|
||||||
|
|
|
@ -27,7 +27,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
} else {
|
} else {
|
||||||
const images = await prisma.image.findMany({
|
const images = await prisma.image.findMany({
|
||||||
where: {
|
where: {
|
||||||
user
|
userId: user.id
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
created_at: true,
|
created_at: true,
|
||||||
|
|
|
@ -31,14 +31,49 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
||||||
data: { embedColor: req.body.embedColor }
|
data: { embedColor: req.body.embedColor }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (req.body.systemTheme) await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: { systemTheme: req.body.systemTheme }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.body.customTheme) {
|
||||||
|
if (user.customTheme) await prisma.user.update({
|
||||||
|
where: { id: user.id },
|
||||||
|
data: {
|
||||||
|
customTheme: {
|
||||||
|
update: {
|
||||||
|
...req.body.customTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}); else await prisma.theme.create({
|
||||||
|
data: {
|
||||||
|
userId: user.id,
|
||||||
|
...req.body.customTheme
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const newUser = await prisma.user.findFirst({
|
const newUser = await prisma.user.findFirst({
|
||||||
where: { id: user.id }
|
where: {
|
||||||
|
id: Number(user.id)
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
administrator: true,
|
||||||
|
embedColor: true,
|
||||||
|
embedTitle: true,
|
||||||
|
id: true,
|
||||||
|
images: false,
|
||||||
|
password: false,
|
||||||
|
systemTheme: true,
|
||||||
|
customTheme: true,
|
||||||
|
token: true,
|
||||||
|
username: true
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Logger.get('user').info(`User ${user.username} (${newUser.username}) (${newUser.id}) was updated`);
|
Logger.get('user').info(`User ${user.username} (${newUser.username}) (${newUser.id}) was updated`);
|
||||||
|
|
||||||
delete newUser.password;
|
|
||||||
|
|
||||||
return res.json(newUser);
|
return res.json(newUser);
|
||||||
} else {
|
} else {
|
||||||
delete user.password;
|
delete user.password;
|
||||||
|
|
Loading…
Reference in a new issue