Frontend: Add Tailwind JIT + Better theme selection
This commit is contained in:
parent
e5483dc888
commit
18601a9b39
@ -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.
|
||||
|
@ -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));
|
@ -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'],
|
||||
|
@ -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));
|
||||
|
@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
@ -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) => {
|
@ -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,
|
||||
|
@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
{ plugin: require('craco-plugin-scoped-css') },
|
||||
{ plugin: require('@dvhb/craco-extend-scope'), options: { path: 'static' } },
|
||||
],
|
||||
}
|
712
frontend/package-lock.json
generated
712
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||
|
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -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
|
||||
|
@ -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) => {
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -21,7 +21,7 @@ const slice = createSlice({
|
||||
config.memberListToggled = value;
|
||||
},
|
||||
}
|
||||
})
|
||||
});
|
||||
const actions = slice.actions;
|
||||
export default slice.reducer;
|
||||
|
||||
|
@ -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(),
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -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 }));
|
||||
|
@ -22,8 +22,7 @@ const slice = createSlice({
|
||||
pings[payload.guildId] = [];
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
});
|
||||
export const actions = slice.actions;
|
||||
export default slice.reducer;
|
||||
|
||||
|
@ -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() }));
|
||||
}
|
@ -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,
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const headers = () => ({
|
||||
export const getHeaders = () => ({
|
||||
'Authorization': `Bearer ${token()}`,
|
||||
});
|
||||
|
||||
|
1516
frontend/src/styles/output.css
Normal file
1516
frontend/src/styles/output.css
Normal file
File diff suppressed because it is too large
Load Diff
3
frontend/src/styles/tailwind.css
Normal file
3
frontend/src/styles/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -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;
|
||||
|
@ -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': {
|
||||
|
18
frontend/tailwind.config.js
Normal file
18
frontend/tailwind.config.js
Normal 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" },
|
||||
},
|
||||
},
|
||||
};
|
2
frontend/types/store.d.ts
vendored
2
frontend/types/store.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
declare namespace Store {
|
||||
import { UserTypes } from '@accord/types';
|
||||
import { Entity, UserTypes } from '@accord/types';
|
||||
|
||||
export interface AppState {
|
||||
auth: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user