Add upload icon to file input.
This commit is contained in:
parent
c820f0d77d
commit
f45f0fadf1
@ -13,6 +13,7 @@ import { ready } from '../store/auth';
|
||||
import { initPings } from '../store/pings';
|
||||
import VerifyPage from './pages/auth/verify-page';
|
||||
import InvitePage from './pages/invite-page';
|
||||
import ThemePage from './pages/theme-page';
|
||||
|
||||
export default function App() {
|
||||
const dispatch = useDispatch();
|
||||
@ -32,6 +33,7 @@ export default function App() {
|
||||
<Route exact path="/verify" component={VerifyPage} />
|
||||
|
||||
<PrivateRoute exact path="/join/:inviteId" component={InvitePage} />
|
||||
<PrivateRoute exact path="/themes/:themeCode" component={ThemePage} />
|
||||
<PrivateRoute exact path="/channels/@me" component={OverviewPage} />
|
||||
<PrivateRoute exact path="/channels/:guildId/:channelId?" component={GuildPage} />
|
||||
|
||||
|
23
frontend/src/components/inputs/file-input.scoped.css
Normal file
23
frontend/src/components/inputs/file-input.scoped.css
Normal file
@ -0,0 +1,23 @@
|
||||
span.icon {
|
||||
position: absolute;
|
||||
top: 65%;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
label:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 8px 36px;
|
||||
}
|
@ -1,21 +1,29 @@
|
||||
import './file-input.scoped.css';
|
||||
import { ChangeEventHandler } from 'react';
|
||||
import Input, { InputProps } from './input';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faUpload } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
type FileInputProps = {
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
} & InputProps;
|
||||
|
||||
|
||||
const FileInput: React.FunctionComponent<FileInputProps> = (props) => {
|
||||
return (
|
||||
<Input
|
||||
accept="image/*"
|
||||
className="pt-5"
|
||||
label={props.label ?? 'Image'}
|
||||
register={(): any => {}}
|
||||
type="file"
|
||||
{...props}
|
||||
onChange={props.onChange} />
|
||||
<label>
|
||||
<span className="icon">
|
||||
<FontAwesomeIcon icon={faUpload} />
|
||||
</span>
|
||||
<Input
|
||||
accept="image/*"
|
||||
className="pt-5"
|
||||
label={props.label ?? 'Image'}
|
||||
register={(): any => { }}
|
||||
type="file"
|
||||
{...props}
|
||||
onChange={props.onChange} />
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default FileInput;
|
@ -36,7 +36,7 @@ const GuildSettingsInvites: React.FunctionComponent = () => {
|
||||
</td>
|
||||
<td className='w-0'>
|
||||
<CircleButton
|
||||
type="button"
|
||||
role="button"
|
||||
style={{ borderColor: 'var(--danger)', color: 'var(--danger)' }}
|
||||
onClick={() => dispatch(deleteInvite(i.id))}>X</CircleButton>
|
||||
</td>
|
||||
|
@ -8,6 +8,7 @@ import SaveChanges from '../../utils/save-changes';
|
||||
import Input from '../../inputs/input';
|
||||
import ChannelSelect from '../../inputs/channel-select';
|
||||
import CircleButton from '../../utils/buttons/circle-button';
|
||||
import FileInput from '../../inputs/file-input';
|
||||
|
||||
const GuildSettingsOverview: React.FunctionComponent = () => {
|
||||
const dispatch = useDispatch();
|
||||
@ -39,9 +40,7 @@ const GuildSettingsOverview: React.FunctionComponent = () => {
|
||||
register={register}
|
||||
options={{ value: guild.name }}
|
||||
className="pt-5" />
|
||||
<Input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
<FileInput
|
||||
label="Icon Image"
|
||||
name="iconURL"
|
||||
className="pt-5"
|
||||
|
@ -69,6 +69,15 @@ const UserSettingsThemes: React.FunctionComponent = () => {
|
||||
handleSubmit(onUpdate)(e);
|
||||
};
|
||||
|
||||
const copyCode = () => {
|
||||
const inviteURL = `${process.env.REACT_APP_WEBSITE_URL}/themes/${theme?.id}`;
|
||||
window.navigator.clipboard.writeText(inviteURL);
|
||||
}
|
||||
|
||||
const shortURL = process.env.REACT_APP_WEBSITE_URL
|
||||
.replace('https://', '')
|
||||
.replace('http://', '');
|
||||
|
||||
const AddTheme: React.FunctionComponent = () => {
|
||||
const [code, setCode] = useState('');
|
||||
|
||||
@ -116,22 +125,23 @@ const UserSettingsThemes: React.FunctionComponent = () => {
|
||||
<header className="mb-5">
|
||||
<h1 className="text-3xl font-bold inline">{theme.name}</h1>
|
||||
</header>
|
||||
<FileInput
|
||||
disabled
|
||||
// disabled={true}!selfIsManager}
|
||||
className="w-1/3"
|
||||
name="icon"
|
||||
label="Icon"
|
||||
options={{ value: theme.iconURL }}
|
||||
tooltip="An optional icon for your theme."
|
||||
onChange={(e) => {
|
||||
const file = e.currentTarget?.files?.[0];
|
||||
if (!file) return;
|
||||
<div className="w-1/3">
|
||||
<FileInput
|
||||
disabled
|
||||
// disabled={true}!selfIsManager}
|
||||
name="icon"
|
||||
label="Icon"
|
||||
options={{ value: theme.iconURL }}
|
||||
tooltip="An optional icon for your theme."
|
||||
onChange={(e) => {
|
||||
const file = e.currentTarget?.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
dispatch(uploadFile(file, ({ url }) => {
|
||||
dispatch(updateTheme(themeId, { iconURL: url }));
|
||||
}));
|
||||
}} />
|
||||
dispatch(uploadFile(file, ({ url }) => {
|
||||
dispatch(updateTheme(themeId, { iconURL: url }));
|
||||
}));
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<form
|
||||
onChange={() => dispatch(openSaveChanges(true))}
|
||||
@ -144,7 +154,19 @@ const UserSettingsThemes: React.FunctionComponent = () => {
|
||||
name="name"
|
||||
register={register}
|
||||
options={{ value: theme.name }} />
|
||||
<Input
|
||||
|
||||
<div className="mt-8 bg-bg-secondary w-1/2 h-10 rounded-md p-2">
|
||||
<CircleButton
|
||||
role="button"
|
||||
style={{ color: 'var(--font)', borderColor: 'var(--font)' }}
|
||||
onClick={copyCode}
|
||||
className="float-right py-0">Copy</CircleButton>
|
||||
<span className="text-lg">
|
||||
<span className='muted'>{shortURL + '/join/'}</span>
|
||||
<span className='primary'>{theme?.code}</span>
|
||||
</span>
|
||||
</div>
|
||||
{/* <Input
|
||||
disabled
|
||||
// disabled={!selfIsManager}
|
||||
tooltip="The code that is used to share themes."
|
||||
@ -152,7 +174,7 @@ const UserSettingsThemes: React.FunctionComponent = () => {
|
||||
label="Code"
|
||||
name="code"
|
||||
register={register}
|
||||
options={{ value: theme.code }} />
|
||||
options={{ value: theme.code }} /> */}
|
||||
</div>
|
||||
|
||||
<div className='mt-2'>
|
||||
|
@ -55,7 +55,7 @@ const InvitePage: React.FunctionComponent<InvitePageProps> = () => {
|
||||
<Wrapper>
|
||||
<NotFoundIcon />
|
||||
<h1 className="text-xl font-bold warning">Invite not found...</h1>
|
||||
<p className="lead">The invite either has expired, or never existed.</p>
|
||||
<p className="lead">The is not the invite you were looking for.</p>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
@ -87,7 +87,7 @@ const InvitePage: React.FunctionComponent<InvitePageProps> = () => {
|
||||
history.push(`/channels/${invite.guildId}`);
|
||||
}}
|
||||
className="bg-success dark">Join</NormalButton>
|
||||
<Link to="/">
|
||||
<Link to="/channels/@me">
|
||||
<NormalButton className="bg-danger light">Cancel</NormalButton>
|
||||
</Link>
|
||||
</div>
|
||||
|
92
frontend/src/components/pages/theme-page.tsx
Normal file
92
frontend/src/components/pages/theme-page.tsx
Normal file
@ -0,0 +1,92 @@
|
||||
import { Entity } from '@acrd/types';
|
||||
import { faSearchLocation } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector, useStore } from 'react-redux';
|
||||
import { Link, useHistory, useParams } from 'react-router-dom';
|
||||
import fetchEntities from '../../store/actions/fetch-entities';
|
||||
import { getGuild, getGuildMembers } from '../../store/guilds';
|
||||
import { joinGuild } from '../../store/members';
|
||||
import { applyTheme, getThemeByCode, getTheme, unlockTheme } from '../../store/themes';
|
||||
import { getUser } from '../../store/users';
|
||||
import SidebarIcon from '../navigation/sidebar/sidebar-icon';
|
||||
import FoundUsername from '../user/username';
|
||||
import NormalButton from '../utils/buttons/normal-button';
|
||||
import FullParticles from '../utils/full-particles';
|
||||
import PageWrapper from './page-wrapper';
|
||||
|
||||
interface ThemePageProps { }
|
||||
|
||||
const ThemePage: React.FunctionComponent<ThemePageProps> = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { themeCode }: any = useParams();
|
||||
const theme: Entity.Theme = useSelector(getThemeByCode(themeCode));
|
||||
const creatorUser: Entity.User = useSelector(getUser(theme?.creatorId));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(unlockTheme(themeCode));
|
||||
}, []);
|
||||
|
||||
const Wrapper: React.FunctionComponent = ({ children }) => (
|
||||
<PageWrapper pageTitle={`acrd.app | ${theme?.name} Theme`}>
|
||||
<div className="flex items-center absolute justify-center h-screen left-[35%]">
|
||||
<section className="rounded-md shadow bg-bg-primary p-8 w-[478px]">
|
||||
{children}
|
||||
</section>
|
||||
</div>
|
||||
</PageWrapper>
|
||||
);
|
||||
|
||||
const NotFoundIcon = () => (
|
||||
<FontAwesomeIcon
|
||||
className="float-left mr-2"
|
||||
color="var(--warning)"
|
||||
icon={faSearchLocation}
|
||||
size="2x" />
|
||||
);
|
||||
|
||||
if (!theme)
|
||||
return (
|
||||
<Wrapper>
|
||||
<NotFoundIcon />
|
||||
<h1 className="text-xl font-bold warning">Theme not found...</h1>
|
||||
<p className="lead">This is not the theme you are looking for.</p>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<FullParticles />
|
||||
<h1 className="text-3xl font-bold text-center">Unlocked '{theme.name}'!</h1>
|
||||
<div className="flex mt-5">
|
||||
<SidebarIcon
|
||||
name={theme.name}
|
||||
imageURL={theme.iconURL}
|
||||
childClasses="bg-bg-tertiary w-24 h-24 pt-6 text-xl"
|
||||
disableHoverEffect />
|
||||
<div className="flex justify-around items-center w-full mx-5">
|
||||
{/* <span className='text-center'>
|
||||
<div className="heading font-bold text-center">Members</div>
|
||||
<code>{members.length}</code>
|
||||
</span> */}
|
||||
<span className='text-center'>
|
||||
<div className="heading font-bold">Owned By</div>
|
||||
<FoundUsername user={creatorUser} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center gap-5 mx-5 mt-5">
|
||||
<NormalButton
|
||||
onClick={() => {
|
||||
applyTheme(themeCode);
|
||||
}}
|
||||
className="bg-success dark">Apply</NormalButton>
|
||||
<Link to="/channels/@me">
|
||||
<NormalButton className="bg-danger light">Cancel</NormalButton>
|
||||
</Link>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default ThemePage;
|
@ -1,6 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { HTMLAttributes } from 'react';
|
||||
|
||||
const CircleButton: React.FunctionComponent<any> = (props) => {
|
||||
const CircleButton: React.FunctionComponent<HTMLAttributes<HTMLButtonElement>> = (props) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
|
@ -1,6 +1,6 @@
|
||||
/* eslint import/no-webpack-loader-syntax: off */
|
||||
import { Entity, REST } from '@acrd/types';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import { actions as api } from './api';
|
||||
import { notInArray } from './utils/filter';
|
||||
import { getHeaders } from './utils/rest-headers';
|
||||
@ -60,6 +60,11 @@ export const getTheme = (id: string, themes: Entity.Theme[]) => {
|
||||
return themes.find(t => t.id === id);
|
||||
}
|
||||
|
||||
export const getThemeByCode = (code: string) => createSelector(
|
||||
state => state.entities.themes,
|
||||
themes => themes.find(t => t.code === code)
|
||||
);
|
||||
|
||||
export const createTheme = (theme: Partial<Entity.Theme>, callback?: (theme: Entity.Theme) => any) => (dispatch) => {
|
||||
dispatch(api.restCallBegan({
|
||||
url: '/themes',
|
||||
|
Loading…
x
Reference in New Issue
Block a user