diff --git a/README.md b/README.md
index 4f65c21..87b4e7b 100644
--- a/README.md
+++ b/README.md
@@ -17,9 +17,8 @@ Fast & lightweight file uploading.
- Configurable
- Fast
- Built with Next.js & React
-- Support for **multible database types**
- 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
diff --git a/package.json b/package.json
index 7aa54c8..be2072c 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "zip3",
- "version": "3.1.0",
+ "version": "3.2.0",
"scripts": {
"prepare": "husky install",
"dev": "NODE_ENV=development node server",
diff --git a/prisma/migrations/20210826034827_custom_themes/migration.sql b/prisma/migrations/20210826034827_custom_themes/migration.sql
new file mode 100644
index 0000000..6d1b708
--- /dev/null
+++ b/prisma/migrations/20210826034827_custom_themes/migration.sql
@@ -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;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 147caec..9d1ae16 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -13,12 +13,29 @@ model User {
password String
token String
administrator Boolean @default(false)
+ systemTheme String @default("dark_blue")
+ customTheme Theme?
embedTitle String?
embedColor String @default("#2f3136")
images Image[]
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 {
id Int @id @default(autoincrement())
file String
@@ -31,25 +48,25 @@ model Image {
}
model InvisibleImage {
- id Int
- image Image @relation(fields: [id], references: [id])
+ id Int
+ image Image @relation(fields: [id], references: [id])
- invis String @unique
+ invis String @unique
}
model Url {
- id Int @id @default(autoincrement())
+ id Int @id @default(autoincrement())
to String
- created_at DateTime @default(now())
- views Int @default(0)
+ created_at DateTime @default(now())
+ views Int @default(0)
invisible InvisibleUrl?
- user User @relation(fields: [userId], references: [id])
+ user User @relation(fields: [userId], references: [id])
userId Int
}
model InvisibleUrl {
- id Int
- url Url @relation(fields: [id], references: [id])
+ id Int
+ url Url @relation(fields: [id], references: [id])
- invis String @unique
+ invis String @unique
}
diff --git a/scripts/deploy-db.js b/scripts/deploy-db.js
index 5336b1d..da31b55 100644
--- a/scripts/deploy-db.js
+++ b/scripts/deploy-db.js
@@ -3,9 +3,8 @@ const prismaRun = require('./prisma-run');
module.exports = async (config) => {
try {
- await prismaRun(config.database.url, ['migrate', 'deploy', '--schema=prisma/schema.prisma']);
- await prismaRun(config.database.url, ['generate', '--schema=prisma/schema.prisma']);
- await prismaRun(config.database.url, ['db', 'seed', '--preview-feature', '--schema=prisma/schema.prisma']);
+ await prismaRun(config.database.url, ['migrate', 'deploy']);
+ await prismaRun(config.database.url, ['generate']);
} catch (e) {
console.log(e);
Logger.get('db').error('there was an error.. exiting..');
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 1d7457e..a62c72b 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -23,6 +23,7 @@ import {
DialogContent,
DialogContentText,
DialogTitle,
+ Select,
} from '@material-ui/core';
import {
Menu as MenuIcon,
@@ -33,10 +34,15 @@ import {
ContentCopy as CopyIcon,
Autorenew as ResetIcon,
Logout as LogoutIcon,
- PeopleAlt as UsersIcon
+ PeopleAlt as UsersIcon,
+ Brush as BrushIcon
} from '@material-ui/icons';
import copy from 'copy-to-clipboard';
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 = [
{
@@ -122,11 +128,14 @@ function ResetTokenDialog({ open, setOpen, setToken }) {
}
export default function Layout({ children, user, loading, noPaper }) {
+ const [systemTheme, setSystemTheme] = useState(user.systemTheme || 'dark_blue');
const [mobileOpen, setMobileOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [copyOpen, setCopyOpen] = useState(false);
const [resetOpen, setResetOpen] = useState(false);
const [token, setToken] = useState(user?.token);
+ const router = useRouter();
+ const dispatch = useStoreDispatch();
const open = Boolean(anchorEl);
const handleClick = e => setAnchorEl(e.currentTarget);
@@ -142,6 +151,17 @@ export default function Layout({ children, user, loading, noPaper }) {
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 = (
@@ -221,6 +241,22 @@ export default function Layout({ children, user, loading, noPaper }) {
+
)}
diff --git a/src/components/Theming.tsx b/src/components/Theming.tsx
new file mode 100644
index 0000000..ac68710
--- /dev/null
+++ b/src/components/Theming.tsx
@@ -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 (
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/pages/Dashboard.tsx b/src/components/pages/Dashboard.tsx
index a30c172..a8b1b2b 100644
--- a/src/components/pages/Dashboard.tsx
+++ b/src/components/pages/Dashboard.tsx
@@ -24,6 +24,7 @@ type Aligns = 'inherit' | 'right' | 'left' | 'center' | 'justify';
export function bytesToRead(bytes: number) {
if (isNaN(bytes)) return '0.0 B';
+ if (bytes === Infinity) return '0.0 B';
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
let num = 0;
@@ -96,7 +97,7 @@ export default function Dashboard() {
const imgs = await useFetch('/api/user/images');
const stts = await useFetch('/api/stats');
setImages(imgs);
- setStats(stts);
+ setStats(stts);console.log(stts);
setApiLoading(false);
};
diff --git a/src/components/pages/Manage.tsx b/src/components/pages/Manage.tsx
index 64cf033..e20b263 100644
--- a/src/components/pages/Manage.tsx
+++ b/src/components/pages/Manage.tsx
@@ -1,5 +1,5 @@
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 * as yup from 'yup';
@@ -8,6 +8,7 @@ import Backdrop from 'components/Backdrop';
import Alert from 'components/Alert';
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
import { updateUser } from 'lib/redux/reducers/user';
+import { useRouter } from 'next/router';
const validationSchema = yup.object({
username: yup
@@ -15,6 +16,45 @@ const validationSchema = yup.object({
.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 }) {
return (
state.user);
const dispatch = useStoreDispatch();
+ const router = useRouter();
const [loading, setLoading] = 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 (
<>
@@ -104,7 +180,42 @@ export default function Manage() {
+ >Save User
+
+
+ Manage Theme
+
>
diff --git a/src/lib/middleware/withZipline.ts b/src/lib/middleware/withZipline.ts
index 62a7a23..25e8f12 100644
--- a/src/lib/middleware/withZipline.ts
+++ b/src/lib/middleware/withZipline.ts
@@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { CookieSerializeOptions } from 'cookie';
-import type { User } from '@prisma/client';
+import type { Image, Theme, User } from '@prisma/client';
import { serialize } from 'cookie';
import { sign64, unsign64 } from '../util';
@@ -17,7 +17,18 @@ export interface NextApiFile {
}
export type NextApiReq = NextApiRequest & {
- user: () => Promise;
+ 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;
cleanCookie: (name: string) => void;
file?: NextApiFile;
@@ -83,6 +94,18 @@ export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse)
const user = await prisma.user.findFirst({
where: {
id: Number(userId)
+ },
+ select: {
+ administrator: true,
+ embedColor: true,
+ embedTitle: true,
+ id: true,
+ images: true,
+ password: true,
+ systemTheme: true,
+ customTheme: true,
+ token: true,
+ username: true
}
});
diff --git a/src/lib/redux/reducers/user.ts b/src/lib/redux/reducers/user.ts
index b86472c..7e2e125 100644
--- a/src/lib/redux/reducers/user.ts
+++ b/src/lib/redux/reducers/user.ts
@@ -1,3 +1,4 @@
+import { Theme } from '@prisma/client';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface User {
@@ -5,6 +6,8 @@ export interface User {
token: string;
embedTitle: string;
embedColor: string;
+ systemTheme: string;
+ customTheme?: Theme;
}
const initialState: User = null;
diff --git a/src/lib/themes/ayu_dark.ts b/src/lib/themes/ayu_dark.ts
new file mode 100644
index 0000000..3da6216
--- /dev/null
+++ b/src/lib/themes/ayu_dark.ts
@@ -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'
+ }
+});
\ No newline at end of file
diff --git a/src/lib/themes/dark.ts b/src/lib/themes/dark.ts
new file mode 100644
index 0000000..e7a2323
--- /dev/null
+++ b/src/lib/themes/dark.ts
@@ -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'
+ }
+});
\ No newline at end of file
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 2a994fd..3b5b46b 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -2,9 +2,7 @@ import React from 'react';
import { Provider } from 'react-redux';
import PropTypes from 'prop-types';
import Head from 'next/head';
-import { ThemeProvider } from '@material-ui/core/styles';
-import CssBaseline from '@material-ui/core/CssBaseline';
-import theme from 'lib/themes/dark_blue';
+import Theming from 'components/Theming';
import { useStore } from 'lib/redux/store';
export default function MyApp({ Component, pageProps }) {
@@ -22,10 +20,10 @@ export default function MyApp({ Component, pageProps }) {
-
-
-
-
+
);
}
diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts
index 5c46199..a017e25 100644
--- a/src/pages/api/auth/login.ts
+++ b/src/pages/api/auth/login.ts
@@ -2,11 +2,18 @@ import prisma from 'lib/prisma';
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
import { checkPassword } from 'lib/util';
import Logger from 'lib/logger';
+import prismaRun from '../../../../scripts/prisma-run';
+import config from 'lib/config';
async function handler(req: NextApiReq, res: NextApiRes) {
if (req.method !== 'POST') return res.status(405).end();
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({
where: {
username
diff --git a/src/pages/api/user/images.ts b/src/pages/api/user/images.ts
index e1ed881..665ba46 100644
--- a/src/pages/api/user/images.ts
+++ b/src/pages/api/user/images.ts
@@ -27,7 +27,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
} else {
const images = await prisma.image.findMany({
where: {
- user
+ userId: user.id
},
select: {
created_at: true,
diff --git a/src/pages/api/user/index.ts b/src/pages/api/user/index.ts
index ea87a71..57a361c 100644
--- a/src/pages/api/user/index.ts
+++ b/src/pages/api/user/index.ts
@@ -31,14 +31,49 @@ async function handler(req: NextApiReq, res: NextApiRes) {
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({
- 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`);
- delete newUser.password;
-
return res.json(newUser);
} else {
delete user.password;