wip
This commit is contained in:
parent
11f185bf72
commit
7c833c3568
8 changed files with 161 additions and 9 deletions
|
@ -66,6 +66,7 @@
|
|||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.12.0",
|
||||
"react-image-crop": "^9.0.5",
|
||||
"react-modal-hook": "^3.0.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router-dom": "^5.2.1",
|
||||
|
|
|
@ -31,7 +31,6 @@ export const AccountsPage: React.VFC = () => {
|
|||
</div>
|
||||
) : (
|
||||
<article className="fade">
|
||||
<h1 className="text-125 text-bold">{data.username}<span className="text-dimmed">@{data.host}</span></h1>
|
||||
<div>
|
||||
<strong>{t('_accounts.switchAccount')}</strong>
|
||||
</div>
|
||||
|
|
10
src/frontend/components/CurrentUser.tsx
Normal file
10
src/frontend/components/CurrentUser.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { useGetSessionQuery } from '../services/session';
|
||||
import { Skeleton } from './Skeleton';
|
||||
|
||||
export const CurrentUser: React.VFC = () => {
|
||||
const {data} = useGetSessionQuery(undefined);
|
||||
return data ? (
|
||||
<h1 className="text-125">{data.username}<span className="text-dimmed">@{data.host}</span></h1>
|
||||
) : <Skeleton height="1.5rem" />;
|
||||
};
|
|
@ -1,18 +1,20 @@
|
|||
import React from 'react';
|
||||
import React, { HTMLProps } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export type HeaderProps = {
|
||||
hasTopLink?: boolean;
|
||||
className?: HTMLProps<HTMLElement>['className'],
|
||||
style?: HTMLProps<HTMLElement>['style'],
|
||||
};
|
||||
|
||||
const messageNumber = Math.floor(Math.random() * 6) + 1;
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({hasTopLink, children}) => {
|
||||
export const Header: React.FC<HeaderProps> = ({hasTopLink, children, className, style}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<header className={'xarticle card mt-5 mb-3'}>
|
||||
<header className={`card ${className ?? ''}`} style={style}>
|
||||
<div className="body">
|
||||
<h1 className="text-primary mb-0" style={{ fontSize: '2rem' }}>
|
||||
{hasTopLink ? <Link to="/">{t('title')}</Link> : t('title')}
|
||||
|
|
92
src/frontend/components/NekomimiPage.tsx
Normal file
92
src/frontend/components/NekomimiPage.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import React, { ChangeEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ReactCrop, { Crop } from 'react-image-crop';
|
||||
|
||||
import 'react-image-crop/dist/ReactCrop.css';
|
||||
|
||||
export const NekomimiPage: React.VFC = () => {
|
||||
const {t} = useTranslation();
|
||||
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
||||
const [image, setImage] = useState<HTMLImageElement | null>(null);
|
||||
const [crop, setCrop] = useState<Partial<Crop>>({unit: '%', width: 100, aspect: 1 / 1});
|
||||
const [completedCrop, setCompletedCrop] = useState<Crop>();
|
||||
|
||||
const previewCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const onChangeFile: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||||
if (e.target.files === null || e.target.files.length === 0) return;
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => setBlobUrl(reader.result as string));
|
||||
reader.readAsDataURL(e.target.files[0]);
|
||||
setCrop({unit: '%', width: 100, aspect: 1 / 1});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!completedCrop || !previewCanvasRef.current || !image) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = previewCanvasRef.current;
|
||||
const crop = completedCrop;
|
||||
|
||||
const scaleX = image.naturalWidth / image.width;
|
||||
const scaleY = image.naturalHeight / image.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
const pixelRatio = window.devicePixelRatio;
|
||||
|
||||
canvas.width = crop.width * pixelRatio * scaleX;
|
||||
canvas.height = crop.height * pixelRatio * scaleY;
|
||||
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
|
||||
ctx.drawImage(
|
||||
image,
|
||||
crop.x * scaleX,
|
||||
crop.y * scaleY,
|
||||
crop.width * scaleX,
|
||||
crop.height * scaleY,
|
||||
0,
|
||||
0,
|
||||
crop.width * scaleX,
|
||||
crop.height * scaleY
|
||||
);
|
||||
}, [completedCrop]);
|
||||
|
||||
return (
|
||||
<div className="fade">
|
||||
<h2>ネコミミアジャスター</h2>
|
||||
<input type="file" className="input-field" accept="image/*" onChange={onChangeFile} />
|
||||
{blobUrl && (
|
||||
<div className="row mt-2">
|
||||
<div className="col-8 col-12-sm">
|
||||
<ReactCrop src={blobUrl} crop={crop}
|
||||
onImageLoaded={(i) => setImage(i)}
|
||||
onChange={(c) => setCrop(c)}
|
||||
onComplete={(c) => setCompletedCrop(c)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-4 col-12-sm">
|
||||
<h3 className="text-100 text-bold">プレビュー</h3>
|
||||
<div className="cat mt-4 mb-2" style={{position: 'relative', width: 96, height: 96}}>
|
||||
<canvas
|
||||
ref={previewCanvasRef}
|
||||
className="circle"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
zIndex: 100,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn primary">アップロード</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -13,6 +13,9 @@ import { setAccounts } from '../store/slices/screen';
|
|||
import { useGetSessionQuery } from '../services/session';
|
||||
import { AdminPage } from '../components/AdminPage';
|
||||
import { $get } from '../misc/api';
|
||||
import { NekomimiPage } from '../components/NekomimiPage';
|
||||
import { CurrentUser } from '../components/CurrentUser';
|
||||
import { Card } from '../components/Card';
|
||||
|
||||
export const IndexSessionPage: React.VFC = () => {
|
||||
const [selectedTab, setSelectedTab] = useState<string>('misshai');
|
||||
|
@ -29,6 +32,7 @@ export const IndexSessionPage: React.VFC = () => {
|
|||
const it: TabItem[] = [];
|
||||
it.push({ label: t('_nav.misshai'), key: 'misshai' });
|
||||
it.push({ label: t('_nav.accounts'), key: 'accounts' });
|
||||
it.push({ label: 'ネコミミ', key: 'nekomimi' });
|
||||
if (data?.isAdmin) {
|
||||
it.push({ label: 'Admin', key: 'admin' });
|
||||
}
|
||||
|
@ -41,6 +45,7 @@ export const IndexSessionPage: React.VFC = () => {
|
|||
case 'misshai': return <MisshaiPage />;
|
||||
case 'accounts': return <AccountsPage />;
|
||||
case 'admin': return <AdminPage />;
|
||||
case 'nekomimi': return <NekomimiPage />;
|
||||
case 'settings': return <SettingPage/>;
|
||||
default: return null;
|
||||
}
|
||||
|
@ -48,11 +53,13 @@ export const IndexSessionPage: React.VFC = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className="xarticle card" style={{borderRadius: 'var(--radius)'}}>
|
||||
<Tab items={items} selected={selectedTab} onSelect={setSelectedTab}/>
|
||||
<div className="xarticle vgroup shadow-4" style={{position: 'sticky', top: 0, zIndex: 100}}>
|
||||
<Header />
|
||||
<div className="card">
|
||||
<Tab items={items} selected={selectedTab} onSelect={setSelectedTab}/>
|
||||
</div>
|
||||
</div>
|
||||
<article className="xarticle mt-4">
|
||||
<article className="xarticle mt-2">
|
||||
{component}
|
||||
</article>
|
||||
</>
|
||||
|
|
|
@ -6,7 +6,7 @@ body {
|
|||
|
||||
.xarticle {
|
||||
margin: auto;
|
||||
max-width: 720px;
|
||||
max-width: 1024px;
|
||||
}
|
||||
|
||||
.fade {
|
||||
|
@ -96,3 +96,32 @@ small {
|
|||
|
||||
.login-form {
|
||||
}
|
||||
|
||||
|
||||
.cat {
|
||||
&:before, &:after {
|
||||
background: #df548f;
|
||||
border: solid 4px currentColor;
|
||||
box-sizing: border-box;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
&:before {
|
||||
border-radius: 0 75% 75%;
|
||||
transform: rotate(37.5deg) skew(30deg);
|
||||
}
|
||||
&:after {
|
||||
border-radius: 75% 0 75% 75%;
|
||||
transform: rotate(-37.5deg) skew(-30deg);
|
||||
}
|
||||
&.animated:hover {
|
||||
&:before {
|
||||
animation: earwiggleleft 1s infinite;
|
||||
}
|
||||
&:after {
|
||||
animation: earwiggleright 1s infinite;
|
||||
}
|
||||
}
|
||||
}
|
12
yarn.lock
12
yarn.lock
|
@ -1346,6 +1346,11 @@ clone-response@^1.0.2:
|
|||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
clsx@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||
|
||||
co-body@^6.0.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/co-body/-/co-body-6.1.0.tgz#d87a8efc3564f9bfe3aced8ef5cd04c7a8766547"
|
||||
|
@ -4335,6 +4340,13 @@ react-i18next@^11.12.0:
|
|||
"@babel/runtime" "^7.14.5"
|
||||
html-parse-stringify "^3.0.1"
|
||||
|
||||
react-image-crop@^9.0.5:
|
||||
version "9.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-image-crop/-/react-image-crop-9.0.5.tgz#a6dfe5411156f1dd1e435b128424ccf175a86948"
|
||||
integrity sha512-J5lbsBMI36GsbkRHXNEo95OjwpxfkUBrEl9Oxb7z5mDC7iamySXYKhkv9QoidkfmI9e8Vj7q3SgNCVfuZHQMQw==
|
||||
dependencies:
|
||||
clsx "^1.1.1"
|
||||
|
||||
react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue