Frontend: Add Tailwind JIT + Better theme selection

This commit is contained in:
ADAMJR 2021-12-28 16:12:44 +00:00
parent e5483dc888
commit 18601a9b39
37 changed files with 2422 additions and 168 deletions

View File

@ -7,8 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.0] - 26-Dec-21
### Added
- Create, share, and customize themes.

View File

@ -1,9 +1,9 @@
import { Entity } from '@accord/types';
import { Document, model, Schema } from 'mongoose';
import patterns from '@accord/types/patterns';
import { createdAtToDate, useId } from '../../utils/utils';
import validators from '../../utils/validators';
import { generateSnowflake } from '../snowflake-entity';
import { Entity } from '@accord/types';
export interface GuildDocument extends Document, Entity.Guild {
id: string;
@ -11,19 +11,13 @@ export interface GuildDocument extends Document, Entity.Guild {
}
export const Guild = model<GuildDocument>('guild', new Schema({
_id: {
type: String,
default: generateSnowflake,
},
_id: { type: String, default: generateSnowflake },
name: {
type: String,
required: [true, 'Name is required'],
maxlength: [32, 'Name is too long'],
},
createdAt: {
type: Date,
get: createdAtToDate,
},
createdAt: { type: Date, get: createdAtToDate },
iconURL: String,
ownerId: {
type: String,
@ -33,7 +27,7 @@ export const Guild = model<GuildDocument>('guild', new Schema({
systemChannelId: {
type: String,
validate: [validators.optionalSnowflake, 'Invalid Snowflake ID'],
}
},
},
{ toJSON: { getters: true } })
.method('toClient', useId));

View File

@ -1,17 +1,19 @@
import { Entity } from '@accord/types';
import patterns from '@accord/types/patterns';
import { Document, model, Schema } from 'mongoose';
import { createdAtToDate, useId } from '../../utils/utils';
import { useId } from '../../utils/utils';
import { generateSnowflake } from '../snowflake-entity';
import generateInvite from '../utils/generate-invite';
export interface ThemeDocument extends Document, Entity.Guild {
export interface ThemeDocument extends Document, Entity.Theme {
id: string;
createdAt: never;
}
export const Theme = model<ThemeDocument>('theme', new Schema({
_id: { type: String, default: generateSnowflake },
createdAt: { type: Date, get: createdAtToDate },
code: { type: String, default: generateInvite, unique: true },
createdAt: { type: Date, default: new Date() },
creatorId: {
type: String,
required: [true, 'Creator ID is required'],

View File

@ -1,11 +1,10 @@
import patterns from '@accord/types/patterns';
import { Entity, UserTypes } from '@accord/types';
import { Document, model, Schema } from 'mongoose';
import passportLocalMongoose from 'passport-local-mongoose';
import { createdAtToDate, useId } from '../../utils/utils';
import uniqueValidator from 'mongoose-unique-validator';
import { generateSnowflake } from '../snowflake-entity';
import validators from '../../utils/validators';
import patterns from '@accord/types/patterns';
import { Entity, UserTypes } from '@accord/types';
export interface UserDocument extends Document, Entity.User {
_id: string | never;
@ -57,6 +56,7 @@ export const User = model<UserDocument>('user', new Schema({
email: {
type: String,
unique: [true, 'Email is already in use'],
dropDups: true,
uniqueCaseInsensitive: true,
validate: [validators.optionalPattern('email'), 'Invalid email address'],
},
@ -106,7 +106,6 @@ export const User = model<UserDocument>('user', new Schema({
default: {} as UserTypes.VoiceState,
},
}, { toJSON: { getters: true } })
.plugin(uniqueValidator)
.plugin(passportLocalMongoose, {
usernameField: 'email',
message: 'UserExistsError',
@ -118,7 +117,7 @@ export const User = model<UserDocument>('user', new Schema({
IncorrectPasswordError: 'Password or username are incorrect',
IncorrectUsernameError: 'Password or username are incorrect',
MissingUsernameError: 'No username was given',
UserExistsError: 'Email is in use'
UserExistsError: 'Email is already in use',
}
})
.method('toClient', useId));

View File

@ -2,6 +2,7 @@ import { Entity } from '@accord/types';
import DBWrapper from './db-wrapper';
import { Theme, ThemeDocument } from './models/theme';
import parseCSS from 'css-parse';
import { SelfUserDocument } from './models/user';
export default class Themes extends DBWrapper<string, ThemeDocument> {
public async get(id: string | undefined) {
@ -15,4 +16,26 @@ export default class Themes extends DBWrapper<string, ThemeDocument> {
parseCSS(options.styles);
return await Theme.create(options);
}
public async lock(themeId: string, user: SelfUserDocument) {
const index = user.unlockedThemeIds.indexOf(themeId);
user.unlockedThemeIds.slice(index);
await user.save();
deps.webSocket.handle({
emit: 'USER_UPDATE',
to: [user.id],
send: { unlockedThemeIds: user.unlockedThemeIds },
});
}
public async unlock(themeId: string, user: SelfUserDocument) {
user.unlockedThemeIds.push(themeId);
await user.save();
deps.webSocket.handle({
emit: 'USER_UPDATE',
to: [user.id],
send: { unlockedThemeIds: user.unlockedThemeIds },
});
}
}

View File

@ -1,19 +1,19 @@
import { Application } from 'express-serve-static-core';
import { router as apiRoutes } from '../routes/api-routes';
import { router as authRoutes } from '../routes/auth-routes';
import { router as channelsRoutes } from '../routes/channels-routes';
import { router as guildsRoutes } from '../routes/guilds-routes';
import { router as usersRoutes } from '../routes/users-routes';
import { router as invitesRoutes } from '../routes/invites-routes';
import { router as themesRoutes } from '../routes/themes-routes';
import { router as channelsRoutes } from '../routes/channel-routes';
import { router as guildsRoutes } from '../routes/guild-routes';
import { router as usersRoutes } from '../routes/user-routes';
import { router as invitesRoutes } from '../routes/invite-routes';
import { router as themesRoutes } from '../routes/theme-routes';
import { resolve } from 'path';
import express from 'express';
export default (app: Application, prefix: string) => {
app.get('/', (req, res) => res.redirect(prefix));
app.use(`/assets`, express.static(resolve('./assets')));
app.use(prefix, apiRoutes);
app.use(prefix, apiRoutes);
app.use(`${prefix}/auth`, authRoutes);
app.use(`${prefix}/invites`, invitesRoutes);
app.use(`${prefix}/channels`, channelsRoutes);

View File

@ -1,4 +1,3 @@
import { Entity } from '@accord/types';
import { Router } from 'express';
import { Theme } from '../../data/models/theme';
import { SelfUserDocument } from '../../data/models/user';
@ -14,11 +13,14 @@ router.get('/', async (req, res) => {
});
router.post('/', updateUser, validateUser, async (req, res) => {
const user: SelfUserDocument = res.locals.user;
const theme = await deps.themes.create({
creatorId: res.locals.user.id,
creatorId: user.id,
name: req.body.name,
styles: req.body.styles,
});
await deps.themes.unlock(theme.id, user);
res.status(201).json({ theme });
});
@ -27,6 +29,14 @@ router.get('/:id', async (req, res) => {
res.json(theme);
});
router.get('/:id/unlock', updateUser, validateUser, async (req, res) => {
const theme = await deps.themes.get(req.params.id);
const user: SelfUserDocument = res.locals.user;
await deps.themes.unlock(theme.id, user);
res.json(user.unlockedThemeIds);
});
router.patch('/:id', async (req, res) => {
const theme = await deps.themes.get(req.params.id);
if (res.locals.user.id !== theme.creatorId)
@ -46,15 +56,7 @@ router.delete('/:id', updateUser, validateUser, async (req, res) => {
throw new APIError(403, 'You cannot manage this theme');
await theme.deleteOne();
await deps.themes.lock(theme.id, res.locals.user);
res.status(201).json({ message: 'Deleted' });
});
router.get('/unlock/:id', updateUser, validateUser, async (req, res) => {
const theme = await deps.themes.get(req.params.id);
const user: SelfUserDocument = res.locals.user;
user.unlockedThemeIds.push(theme.id);
await user.save();
res.json(user.unlockedThemeIds);
});

View File

@ -1,4 +1,4 @@
import { REST } from '@accord/types';
import { Entity, REST, UserTypes } from '@accord/types';
import { Router } from 'express';
import { User } from '../../data/models/user';
import generateInvite from '../../data/utils/generate-invite';
@ -8,6 +8,7 @@ import { GuildMember } from '../../data/models/guild-member';
import { Channel } from '../../data/models/channel';
import updateUser from '../middleware/update-user';
import validateUser from '../middleware/validate-user';
import { Theme } from '../../data/models/theme';
export const router = Router();
@ -52,19 +53,28 @@ router.get('/check-email', async (req, res) => {
router.get('/self', updateUser, validateUser, async (req, res) => res.json(res.locals.user));
router.get('/entities', updateUser, validateUser, async (req, res) => {
const $in = res.locals.user.guildIds;
const [channels, guilds, members, roles, unsecureUsers] = await Promise.all([
const user: UserTypes.Self = res.locals.user;
const $in = user.guildIds;
const [channels, guilds, members, roles, themes, unsecureUsers] = await Promise.all([
Channel.find({ guildId: { $in } }),
Guild.find({ _id: { $in } }),
GuildMember.find({ guildId: { $in } }),
Role.find({ guildId: { $in } }),
Theme.find({ _id: { $in: user.unlockedThemeIds } }),
User.find({ guildIds: { $in } }),
]);
const secureUsers = unsecureUsers.map((u: any) => deps.users.secure(u));
res.json({ channels, guilds, members, roles, users: secureUsers } as REST.From.Get['/users/entities']);
res.json({
channels,
guilds,
members,
roles,
themes,
users: secureUsers,
} as REST.From.Get['/users/entities']);
});
router.get('/:id', async (req, res) => {

View File

@ -1,6 +1,6 @@
import { Server } from 'http';
import { Server as SocketServer } from 'socket.io';
import { WSEvent } from './ws-events/ws-event';
import { WSAction, WSEvent } from './ws-events/ws-event';
import { resolve } from 'path';
import { readdirSync } from 'fs';
import { SessionManager } from './modules/session-manager';
@ -45,12 +45,8 @@ export class WebSocket {
client.on(event.on, async (data: any) => {
try {
const actions = await event.invoke.call(event, this, client, data);
for (const action of actions) {
if (!action) continue;
this.io
.to(action.to)
.emit(action.emit, action.send);
}
for (const action of actions)
if (action) this.handle(action);
} catch (error: any) {
client.emit('error', { message: error.message });
} finally {
@ -65,6 +61,12 @@ export class WebSocket {
log.info('Started WebSocket', 'ws');
}
public handle(action: WSAction<keyof WS.From>) {
this.io
.to(action.to)
.emit(action.emit, action.send);
}
public to(...rooms: string[]) {
return this.io.to(rooms) as {
emit: <K extends keyof WS.From>(name: K, args: WS.From[K]) => any,

View File

@ -1,6 +1,5 @@
module.exports = {
plugins: [
{ plugin: require('craco-plugin-scoped-css') },
{ plugin: require('@dvhb/craco-extend-scope'), options: { path: 'static' } },
],
}

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@
"javascript-time-ago": "^2.3.8",
"moment": "^2.29.1",
"notistack": "^1.0.10",
"postcss-cli": "^9.1.0",
"rc-tooltip": "^5.1.1",
"react": "^17.0.2",
"react-contextmenu": "^2.14.0",
@ -44,7 +45,6 @@
"redux-persist": "^6.0.0",
"socket.io-client": "^4.0.0",
"striptags": "^3.2.0",
"tailwindcss": "^2.2.5",
"typescript": "^4.3.5",
"web-vitals": "^1.1.2"
},
@ -57,7 +57,8 @@
"test": "craco test",
"test:unit": "craco test --watchAll=false",
"test:e2e": "cypress open",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"watch:css": "postcss -w src/styles/tailwind.css -o src/styles/output.css"
},
"eslintConfig": {
"extends": [
@ -77,6 +78,7 @@
]
},
"devDependencies": {
"@tailwindcss/jit": "^0.1.18",
"@testing-library/jest-dom": "^5.15.0",
"@types/chance": "^1.1.3",
"@types/clone": "^2.1.1",
@ -89,10 +91,13 @@
"@types/react-router-dom": "^5.1.8",
"@types/react-select": "^4.0.17",
"@types/socket.io-client": "^3.0.0",
"autoprefixer": "^10.4.0",
"chance": "^1.1.8",
"cypress": "^8.7.0",
"dotenv-cli": "^4.0.0",
"postcss": "^8.4.5",
"sazerac": "^2.0.0",
"tailwindcss": "^2.2.19",
"tsparticles": "^1.33.2"
},
"optionalDependencies": {

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -37,7 +37,7 @@ const CreateGuild: React.FunctionComponent = () => {
<NormalButton className="bg-tertiary w-full h-11 mt-8">Join</NormalButton>
</form>
<h3 className="uppercase font-bold mt-10">Make Your Own</h3>
<h3 className="uppercase font-bold mt-10">Create Your Own</h3>
<form onSubmit={handleSubmit2(submitCreate)}>
<Input

View File

@ -39,7 +39,7 @@ const GuildSettingsRoles: React.FunctionComponent = () => {
setHoisted(activeRole.hoisted);
}, [activeRole]);
const RoleDetails = () => {
const RoleDetails = () => {
return (
<form
className="mb-10"
@ -81,7 +81,7 @@ const GuildSettingsRoles: React.FunctionComponent = () => {
className="bg-danger float-right"
type="button">Delete</NormalButton>
</form>
)
);
}
const onSave = (e) => {

View File

@ -1,62 +1,66 @@
import { useEffect } from 'react';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getTheme } from '../../../store/themes';
import { actions as ui } from '../../../store/ui';
import { createTheme, getTheme } from '../../../store/themes';
import { updateSelf } from '../../../store/users';
import PlusIcon from '../../navigation/sidebar/plus-icon';
import SidebarIcon from '../../navigation/sidebar/sidebar-icon';
import Category from '../../utils/category';
import CircleButton from '../../utils/buttons/circle-button';
const UserSettingsThemes: React.FunctionComponent = () => {
const dispatch = useDispatch();
const themes = useSelector((s: Store.AppState) => s.entities.themes);
const user = useSelector((s: Store.AppState) => s.auth.user);
const featuredThemes = themes.filter(t => t.isFeatured);
const themeId = user.activeThemeId;
const [tab, setTab] = useState(themeId);
useEffect(() => {
const themeWrapper = document.querySelector('#themeWrapper')!;
const theme = getTheme(themeId, themes)!;
themeWrapper.innerHTML = (!themeId || themeId === 'default')
? `<style></style>`
: `<style>${theme.styles}</style>`;
? `<style></style>`
: `<style>${theme.styles}</style>`;
}, [themeId]);
const applyTheme = (id: string) => dispatch(updateSelf({ activeThemeId: id }));
return (
<div className="flex flex-col pt-14 px-10 pb-20 h-full mt-1">
<header>
<h1 className="text-xl font-bold inline">Themes</h1>
</header>
<section className="pt-2">
<Category
className="mb-2"
count={featuredThemes.length}
title="Featured" />
{themes.map(t => (
<div
className="w-12 mr-2 float-left"
key={t.id}
onClick={() => applyTheme(t.id)}
title={t.name}>
<SidebarIcon
childClasses="bg-bg-secondary"
imageURL={t.iconURL}
name={t.name}
disableHoverEffect />
</div>
))}
const SideIcons = () => (
<div className="flex items-center flex-col">
{themes.map(t => (
<div
className="w-12 float-left"
onClick={() => dispatch(ui.openedModal('CreateTheme'))}>
<PlusIcon disableHoverEffect />
key={t.id}
className="w-12"
onClick={() => {
setTab(t.id);
applyTheme(t.id);
}}
title={t.name}>
<SidebarIcon
childClasses={classNames('bg-bg-secondary', {
'border-2 border-primary h-[3.1rem]': t.id === user.activeThemeId,
})}
imageURL={t.iconURL}
name={t.name}
disableHoverEffect />
</div>
</section>
))}
<CircleButton
className="m-2"
onClick={() => dispatch(createTheme('New Theme'))}
style={{ color: 'var(--success)' }}>+</CircleButton>
</div>
);
const ThemeDetails = () => (tab) ? (
<div className="">
</div>
) : null;
return (
<div className="grid grid-cols-12 flex flex-col pt-14 px-10 pb-20 h-full mt-1 gap-8">
<div className="col-span-1"><SideIcons /></div>
<div className="col-span-11"><ThemeDetails /></div>
</div>
);
}

View File

@ -14,6 +14,7 @@ import { actions as messages } from '../store/messages';
import { actions as channels } from '../store/channels';
import { actions as auth, logoutUser } from '../store/auth';
import { actions as pings, addPing } from '../store/pings';
import { actions as themes } from '../store/themes';
import { useSnackbar } from 'notistack';
import events from '../services/event-service';
import { startVoiceFeedback, stopVoiceFeedback } from '../services/voice-service';

View File

@ -1,5 +1,5 @@
@import './theme/accord-theme.css';
@import '~tailwindcss/dist/tailwind.min.css';
@import './styles/output.css';
@import '~@fortawesome/fontawesome-svg-core/styles.css';
body {

View File

@ -29,7 +29,7 @@ export class MentionService {
.replace(this.patterns.original.user, (og, tag) => {
const user = getUserByTag(tag)(this.state);
return (user) ? `<@${user.id}>` : og;
})
});
}
public toHTML(content: string) {

View File

@ -5,19 +5,21 @@ import { actions as guildActions } from '../guilds';
import { actions as memberActions } from '../members';
import { actions as meta } from '../meta';
import { actions as roleActions } from '../roles';
import { actions as themes } from '../themes';
import { actions as userActions } from '../users';
import { headers } from '../utils/rest-headers';
import { getHeaders } from '../utils/rest-headers';
export default () => (dispatch) => {
dispatch(api.restCallBegan({
onSuccess: [],
headers: headers(),
headers: getHeaders(),
url: `/users/entities`,
callback: (data: REST.From.Get['/users/entities']) => {
dispatch(channelActions.fetched(data.channels));
dispatch(guildActions.fetched(data.guilds));
dispatch(memberActions.fetched(data.members));
dispatch(roleActions.fetched(data.roles));
dispatch(themes.fetched(data.themes));
dispatch(userActions.fetched(data.users));
dispatch(meta.fetchedEntities());
},

View File

@ -1,6 +1,6 @@
import { REST, WS } from '@accord/types';
import { createAction } from '@reduxjs/toolkit';
import { headers } from './utils/rest-headers';
import { getHeaders } from './utils/rest-headers';
export const actions = {
restCallBegan: createAction<APIArgs>('api/restCallBegan'),
@ -34,7 +34,7 @@ export const uploadFile = (file: File, callback?: (args: REST.From.Post['/upload
url: '/upload',
data: formData,
headers: {
...headers(),
...getHeaders(),
'Content-Type': 'multipart/form-data',
},
callback,

View File

@ -21,7 +21,7 @@ const slice = createSlice({
config.memberListToggled = value;
},
}
})
});
const actions = slice.actions;
export default slice.reducer;

View File

@ -2,7 +2,7 @@ import { REST, WS, Entity } from '@accord/types';
import { createSlice, createSelector } from '@reduxjs/toolkit';
import { actions as api, uploadFile } from './api';
import { notInArray } from './utils/filter';
import { headers } from './utils/rest-headers';
import { getHeaders } from './utils/rest-headers';
const slice = createSlice({
name: 'messages',
@ -43,7 +43,7 @@ export const fetchMessages = (channelId: string, back = 25) => (dispatch, getSta
dispatch(api.restCallBegan({
onSuccess: [actions.fetched.type],
url: `/channels/${channelId}/messages?back=${back}`,
headers: headers(),
headers: getHeaders(),
}));
}

View File

@ -25,7 +25,7 @@ export default (store) => (next) => async (action) => {
store.dispatch({ type, payload });
// called after dispatch events
callback && await callback(payload);
if (callback) await callback(payload);
} catch (error) {
const response = (error as any).response;
store.dispatch(actions.restCallFailed({ url, response }));

View File

@ -22,8 +22,7 @@ const slice = createSlice({
pings[payload.guildId] = [];
},
},
})
});
export const actions = slice.actions;
export default slice.reducer;

View File

@ -1,5 +1,8 @@
import { Entity } from '@accord/types';
import { Entity, REST } from '@accord/types';
import { createSlice } from '@reduxjs/toolkit';
import { actions as api } from './api';
import { notInArray } from './utils/filter';
import { getHeaders } from './utils/rest-headers';
const slice = createSlice({
name: 'themes',
@ -12,15 +15,59 @@ const slice = createSlice({
styles: '',
}] as Store.AppState['entities']['themes'],
reducers: {
loaded: (themes, { payload }: Store.Action<Entity.Theme>) => {
themes.push(payload);
fetched: (themes, { payload }: Store.Action<Entity.Theme[]>) => {
themes.push(...payload.filter(notInArray(themes)));
},
}
});
export const actions = slice.actions;
export default slice.reducer;
export const getTheme = (id: string, themes: Entity.Theme[]) => {
return themes.find(t => t.id === id);
}
const themeTemplate = `:root {
--primary: #7289da;
--secondary: #99aab5;
--tertiary: #43b582;
--heading: white;
--font: #99aab5;
--normal: #dcddde;
--muted: #72767d;
--link: hsl(197, calc(var(--saturation-factor, 1) * 100%), 47.8%);
--channel: #8e9297;
--saturation-factor: 1;
--success: hsl(139, calc(var(--saturation-factor, 1) * 47.3%), 43.9%);
--danger: hsl(359, calc(var(--saturation-factor, 1) * 82.6%), 59.4%);
--bg-primary: #36393f;
--bg-secondary: #2f3136;
--bg-secondary-alt: #292b2f;
--bg-tertiary: #202225;
--bg-textarea: #40444b;
--bg-modifier-accent: hsla(0, 0%, 100%, 0.06);
--bg-modifier-selected: rgba(79, 84, 92, 0.32);
--bg-floating: #18191c;
--elevation: 0 1px 0 rgba(4, 4, 5, 0.2), 0 1.5px 0 rgba(6, 6, 7, 0.05),
0 2px 0 rgba(4, 4, 5, 0.05);
--font-primary: Whitney, 'Helvetica Neue', Helvetica, Arial, sans-serif;
}`;
export const createTheme = (name: string, styles = themeTemplate, iconURL?: string) => (dispatch) => {
dispatch(api.restCallBegan({
url: '/themes',
method: 'post',
headers: getHeaders(),
data: { name, styles, iconURL } as REST.To.Post['/themes'],
callback: (theme: Entity.Theme) => dispatch(actions.fetched([theme])),
}));
}
export const unlockTheme = (id: string) => (dispatch) => {
dispatch(api.restCallBegan({ url: `/themes/unlock/${id}`, headers: getHeaders() }));
}

View File

@ -1,4 +1,4 @@
import { Entity, WS, REST } from '@accord/types';
import { Entity, WS, REST, UserTypes } from '@accord/types';
import { createSlice, createSelector } from '@reduxjs/toolkit';
import { actions as api, uploadFile } from './api';
import { actions as meta } from './meta';
@ -26,7 +26,7 @@ const slice = createSlice({
export const actions = slice.actions;
export default slice.reducer;
export const updateSelf = (payload: Partial<Entity.User>) => (dispatch) => {
export const updateSelf = (payload: Partial<UserTypes.Self>) => (dispatch) => {
dispatch(api.wsCallBegan({
event: 'USER_UPDATE',
data: { ...payload, token: token() } as WS.Params.UserUpdate,

View File

@ -1,4 +1,4 @@
export const headers = () => ({
export const getHeaders = () => ({
'Authorization': `Bearer ${token()}`,
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -85,11 +85,8 @@ export namespace Entity {
discriminator: number;
guildIds: string[];
premium: boolean;
premiumExpiration: Date;
status: UserTypes.StatusType;
username: string;
activeThemeId: string;
unlockedThemeIds: string[];
voice: UserTypes.VoiceState;
}
}
@ -162,17 +159,18 @@ export namespace UserTypes {
}
export type StatusType = 'ONLINE' | 'OFFLINE';
export interface Self extends Entity.User {
activeThemeId: string;
email: string;
verified: true;
lastReadMessageIds: {
[k: string]: string
};
locked: boolean;
ignored?: {
channelIds: string[];
guildIds: string[];
userIds: string[];
};
lastReadMessageIds: { [k: string]: string };
locked: boolean;
premiumExpiration: Date;
unlockedThemeIds: string[];
verified: true;
}
export interface VoiceState {
channelId?: string;

View File

@ -17,6 +17,7 @@ export namespace REST {
oldPassword: string;
newPassword: string;
}
'/themes': Entity.Theme;
}
}
@ -32,6 +33,7 @@ export namespace REST {
guilds: Entity.Guild[];
members: Entity.GuildMember[];
roles: Entity.Role[];
themes: Entity.Theme[];
users: Entity.User[];
}
'/auth/email/verify-email': {

View File

@ -0,0 +1,18 @@
module.exports = {
mode: "jit",
purge: ["./public/**/*.html", "./src/**/*.{js,jsx,ts,tsx,vue}"],
theme: {
screens: {
sm: "640px",
md: "768px",
lg: "1024px",
xl: "1280px",
"2xl": "1536px",
msm: { max: "640px" },
mmd: { max: "768px" },
mlg: { max: "1024px" },
mxl: { max: "1280px" },
m2xl: { max: "1536px" },
},
},
};

View File

@ -1,5 +1,5 @@
declare namespace Store {
import { UserTypes } from '@accord/types';
import { Entity, UserTypes } from '@accord/types';
export interface AppState {
auth: {