style: use space indentation

feat: no new users allowed
fix: change base language to Korean
fix: change gacha to random text from devs
This commit is contained in:
オスカー、 2024-02-02 01:35:17 +09:00
parent 320dfc0696
commit b6a3b0cd53
35 changed files with 335 additions and 367 deletions

View File

@ -6,4 +6,4 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_size = 4
indent_size = 2

View File

@ -5,16 +5,16 @@ const entities = require('./built/backend/services/db').entities;
const config = Object.freeze(JSON.parse(fs.readFileSync(__dirname + '/config.json', 'utf-8')));
module.exports = {
type: 'postgres',
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
extra: config.db.extra,
entities: entities,
migrations: ['migration/*.ts'],
cli: {
migrationsDir: 'migration'
}
type: 'postgres',
host: config.db.host,
port: config.db.port,
username: config.db.user,
password: config.db.pass,
database: config.db.db,
extra: config.db.extra,
entities: entities,
migrations: ['migration/*.ts'],
cli: {
migrationsDir: 'migration'
}
};

View File

@ -16,35 +16,9 @@ export const misskeyAppInfo = {
permission: [
'read:account',
'write:account',
'read:blocks',
'write:blocks',
'read:drive',
'write:drive',
'read:favorites',
'write:favorites',
'read:following',
'write:following',
'read:messaging',
'write:messaging',
'read:mutes',
'write:mutes',
'write:notes',
'read:notifications',
'write:notifications',
'read:reactions',
'write:reactions',
'write:votes',
'read:pages',
'write:pages',
'write:page-likes',
'read:page-likes',
'read:user-groups',
'write:user-groups',
'read:channels',
'write:channels',
'read:gallery',
'write:gallery',
'read:gallery-likes',
'write:gallery-likes',
],
} as const;

View File

@ -24,5 +24,5 @@ export class UserSetting {
useRanking?: boolean;
@IsOptional()
appendHashtag?: boolean;
appendHashtag?: boolean;
}

View File

@ -180,8 +180,12 @@ router.get('(.*)', async (ctx) => {
async function login(ctx: Context, user: Record<string, unknown>, host: string, token: string) {
const isNewcomer = !(await getUser(user.username as string, host));
await upsertUser(user.username as string, host, token);
if (isNewcomer) {
await die(ctx, 'noNewUserAllowed', 403);
return;
}
await upsertUser(user.username as string, host, token);
const u = await getUser(user.username as string, host);
if (!u) {

View File

@ -38,23 +38,23 @@ export const work = async () => {
await calculateAllRating(groupedUsers);
}
catch (e) {
printLog('Misskey Tools with LycheeBridge 레이팅 계산에 실패했습니다.', 'error');
printLog(e instanceof Error ? errorToString(e) : JSON.stringify(e, null, ' '), 'error');
Store.dispatch({ nowCalculating: false });
printLog('Misskey Tools with LycheeBridge 레이팅 계산에 실패했습니다.', 'error');
printLog(e instanceof Error ? errorToString(e) : JSON.stringify(e, null, ' '), 'error');
Store.dispatch({ nowCalculating: false });
}
finally {
Store.dispatch({ nowCalculating: false });
printLog(`${users.length}개의 계정 레이팅 계산이 완료되었습니다.`);
Store.dispatch({ nowCalculating: false });
printLog(`${users.length}개의 계정 레이팅 계산이 완료되었습니다.`);
}
try {
printLog(`${users.length}개의 계정에 알림을 전송하고 있습니다.`);
printLog(`${users.length}개의 계정에 알림을 전송하고 있습니다.`);
await sendAllAlerts(groupedUsers);
} catch (e) {
printLog('Misskey Tools with LycheeBridge 알림 전송에 실패했습니다.', 'error');
printLog(e instanceof Error ? errorToString(e) : JSON.stringify(e, null, ' '), 'error');
} finally {
printLog('Misskey Tools with LycheeBridge 알림 전송이 완료되었습니다.');
printLog('Misskey Tools with LycheeBridge 알림 전송이 완료되었습니다.');
}
};
@ -75,10 +75,10 @@ const calculateRating = async (host: string, users: User[]) => {
// ユーザーが削除されている場合、レコードからも消してとりやめ
printLog(`${toAcct(user)} 게정이 삭제, 정지, 또는 토큰이 제거된 것으로 보이며, 시스템에서 계정이 제거되었습니다.`, 'warn');
await deleteUser(user.username, user.host);
continue;
continue;
} else {
printLog(`Misskey 오류: ${JSON.stringify(e.error)}`, 'error');
continue;
continue;
}
} else if (e instanceof TimedOutError) {
printLog(`${user.host} 인스턴스로의 연결에 실패하여 레이팅 계산을 중단합니다.`, 'error');
@ -105,7 +105,7 @@ const sendAlerts = async (host: string, users: User[]) => {
.map(user => {
const count = userScoreCache.get(toAcct(user));
if (count == null) return null;
if (count.notesCount - (user.prevNotesCount ?? 0) <= 1) return null;
if (count.notesCount - (user.prevNotesCount ?? 0) <= 1) return null;
return {
user,
count,
@ -121,31 +121,31 @@ const sendAlerts = async (host: string, users: User[]) => {
// 通知
for (const {user, count, message} of models.filter(m => m.user.alertMode === 'notification' || m.user.alertMode === 'both')) {
try {
try {
await sendNotificationAlert(message, user);
} catch (e) {
printLog('Misskey Tools with LycheeBridge 알림 전송에 실패했습니다.', 'error');
} catch (e) {
printLog('Misskey Tools with LycheeBridge 알림 전송에 실패했습니다.', 'error');
printLog(e instanceof Error ? errorToString(e) : JSON.stringify(e, null, ' '), 'error');
} finally {
if (user.alertMode === 'notification') {
await updateScore(user, count);
}
}
} finally {
if (user.alertMode === 'notification') {
await updateScore(user, count);
}
}
}
// アラート
for (const {user, count, message} of models.filter(m => m.user.alertMode === 'note' || m.user.alertMode === 'both')) {
try {
try {
await sendNoteAlert(message, user);
} catch (e) {
printLog('Misskey Tools with LycheeBridge 알림 전송에 실패했습니다.', 'error');
printLog(e instanceof Error ? errorToString(e) : JSON.stringify(e, null, ' '), 'error');
} finally {
await Promise.all([
updateScore(user, count),
delay(1000),
]);
}
} catch (e) {
printLog('Misskey Tools with LycheeBridge 알림 전송에 실패했습니다.', 'error');
printLog(e instanceof Error ? errorToString(e) : JSON.stringify(e, null, ' '), 'error');
} finally {
await Promise.all([
updateScore(user, count),
delay(1000),
]);
}
}
printLog(`${host} 인스턴스의 사용자 ${users.length}명의 알림 전송이 완료되었습니다.`);

View File

@ -1,41 +1,41 @@
doctype html
html
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
block meta
- const title = t ? `${t} | Misskey Tools`: 'Misskey Tools with LycheeBridge';
- const desc = d || '🌠 연합우주의 모든 Misskey 사용자를 위한 다양한 도구 모음집 🚀';
title= title
meta(name='description' content=desc)
meta(property='og:title' content=title)
meta(property='og:site_name' content='Misskey Tools with LycheeBridge')
meta(property='og:description' content=desc)
meta(property='og:type' content='website')
meta(property='og:image' content="https://tools.phater.live/assets/misskey.png")
link(rel="icon", href="/assets/lcb.png", type="image/png")
link(rel="preload", href="/assets/otadesign_rounded.woff")
link(rel="preload", href="/assets/otadesign_rounded.woff2")
link(rel="preload", href="/assets/PretendardJPVariable.woff2")
script(src='https://kit.fontawesome.com/a4464bc6cd.js' crossorigin='anonymous')
link(rel="stylesheet", href="/assets/style.css")
body
#app: .loading 불러오는 중입니다...
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
block meta
- const title = t ? `${t} | Misskey Tools`: 'Misskey Tools with LycheeBridge';
- const desc = d || '🌠 연합우주의 모든 Misskey 사용자를 위한 다양한 도구 모음집 🚀';
title= title
meta(name='description' content=desc)
meta(property='og:title' content=title)
meta(property='og:site_name' content='Misskey Tools with LycheeBridge')
meta(property='og:description' content=desc)
meta(property='og:type' content='website')
meta(property='og:image' content="https://tools.phater.live/assets/misskey.png")
link(rel="icon", href="/assets/lcb.png", type="image/png")
link(rel="preload", href="/assets/otadesign_rounded.woff")
link(rel="preload", href="/assets/otadesign_rounded.woff2")
link(rel="preload", href="/assets/PretendardJPVariable.woff2")
script(src='https://kit.fontawesome.com/a4464bc6cd.js' crossorigin='anonymous')
link(rel="stylesheet", href="/assets/style.css")
body
#app: .loading 불러오는 중입니다...
if token
script.
const token = '#{token}';
const previousToken = localStorage.getItem('token');
const accounts = JSON.parse(localStorage.getItem('accounts') || '[]');
if (previousToken && !accounts.includes(previousToken)) {
accounts.push(previousToken);
}
localStorage.setItem('accounts', JSON.stringify(accounts));
localStorage.setItem('token', token);
history.replaceState(null, null, '/');
if token
script.
const token = '#{token}';
const previousToken = localStorage.getItem('token');
const accounts = JSON.parse(localStorage.getItem('accounts') || '[]');
if (previousToken && !accounts.includes(previousToken)) {
accounts.push(previousToken);
}
localStorage.setItem('accounts', JSON.stringify(accounts));
localStorage.setItem('token', token);
history.replaceState(null, null, '/');
if error
script.
window.__misshaialert = { error: '#{error}' };
if error
script.
window.__misshaialert = { error: '#{error}' };
script(src=`/assets/fe.${version}.js` async defer)
script(src=`/assets/fe.${version}.js` async defer)

View File

@ -1,13 +1,13 @@
doctype html
html
head
meta(charset="UTF-8")
body
p 클라이언트에 문제가 발생했습니다. 해결을 위해 브라우저에 있는 데이터를 제거했습니다.
p 3초 뒤에 메인 페이지로 자동으로 돌아갑니다...
hr
p There's a problem in client and deleted data on device to solve it.
p You will be redirected to the top in 3 seconds.
script.
localStorage.clear();
setTimeout(() => location.href = '/', 3000);
head
meta(charset="UTF-8")
body
p 클라이언트에 문제가 발생했습니다. 해결을 위해 브라우저에 있는 데이터를 제거했습니다.
p 3초 뒤에 메인 페이지로 자동으로 돌아갑니다...
hr
p There's a problem in client and deleted data on device to solve it.
p You will be redirected to the top in 3 seconds.
script.
localStorage.clear();
setTimeout(() => location.href = '/', 3000);

View File

@ -1,25 +1,11 @@
const allKatakana = [
...('アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨ'.split('')),
'ウィ', 'ウェ',
'キャ', 'キュ', 'キョ',
'クァ', 'クォ',
'シャ', 'シュ', 'ショ',
'チャ', 'チュ', 'チョ',
'ヒャ', 'ヒュ', 'ヒョ',
'ミャ', 'ミュ', 'ミョ'
const allTexts: string[] = [
'아무말 빔----',
'휴대폰용 보험은 가입하셨나요?',
];
const allInfix = [ '', 'ー', 'ッ' ];
const getRandomKatakana = () => allKatakana[Math.floor(Math.random() * allKatakana.length)];
const getRandomInfix = () => allInfix[Math.floor(Math.random() * allInfix.length)];
const getRandomText = () => allTexts[Math.floor(Math.random() * allTexts.length)];
export const createGacha = () => {
return [
getRandomKatakana(),
getRandomInfix(),
getRandomKatakana(),
getRandomInfix(),
...(new Array(Math.floor(Math.random() * 2 + 1)).fill('').map(() => getRandomKatakana()))
].join('');
const result = getRandomText();
return result;
};

View File

@ -47,7 +47,7 @@ export const format = (user: IUser, count: Count): string => {
return !v ? m : typeof v === 'function' ? v(score, user) : v;
});
if (user.appendHashtag) {
result = result + '\n\n#misshaialert'
result = result + '\n\n#misshaialert';
}
return result;
};

View File

@ -7,6 +7,7 @@ export const errorCodes = [
'notAuthorized',
'hostNotFound',
'invalidHostFormat',
'noNewUserAllowed',
'other',
] as const;

View File

@ -5,4 +5,4 @@ export const visibilities = [
'users' // ログインユーザー (Groundpolis 限定)
] as const;
export type Visibility = typeof visibilities[number];
export type Visibility = typeof visibilities[number];

View File

@ -10,36 +10,36 @@ import { useSelector } from './store';
import { setDrawerShown } from './store/slices/screen';
const Container = styled.div<IsMobileProp>`
padding: var(--margin);
position: relative;
padding: var(--margin);
position: relative;
`;
const Sidebar = styled.nav`
width: 320px;
position: fixed;
top: var(--margin);
left: var(--margin);
width: 320px;
position: fixed;
top: var(--margin);
left: var(--margin);
`;
const Main = styled.main<IsMobileProp>`
flex: 1;
flex: 1;
margin-top: 80px;
margin-left: ${p => !p.isMobile ? `${320 + 16}px` : 0};
min-width: 0;
margin-left: ${p => !p.isMobile ? `${320 + 16}px` : 0};
min-width: 0;
`;
const MobileHeader = styled.header`
position: fixed;
top: 0;
left: 0;
right: 0;
height: 64px;
background: var(--panel);
z-index: 1000;
> h1 {
font-size: 1rem;
margin-bottom: 0;
}
position: fixed;
top: 0;
left: 0;
right: 0;
height: 64px;
background: var(--panel);
z-index: 1000;
> h1 {
font-size: 1rem;
margin-bottom: 0;
}
`;
export const GeneralLayout: React.FC = ({children}) => {

View File

@ -6,7 +6,7 @@ import { useGetSessionQuery } from './services/session';
import { useSelector } from './store';
export type HeaderProps = {
title?: string;
title?: string;
};
export const Header: React.FC<HeaderProps> = ({title}) => {

View File

@ -1,8 +1,8 @@
import React from 'react';
export type CardProps = {
className?: string;
bodyClassName?: string;
className?: string;
bodyClassName?: string;
};
export const Card: React.FC<CardProps> = ({children, className, bodyClassName}) => {

View File

@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
export type HashtagTimelineProps = {
hashtag: string;
hashtag: string;
};
export const HashtagTimeline: React.VFC<HashtagTimelineProps> = ({hashtag}) => {

View File

@ -7,7 +7,7 @@ const LogItem: React.FC<{log: Log}> = ({log}) => {
return (
<div className={`log ${log.level}`}>
[{time}] {log.text}
[{time}] {log.text}
</div>
);
};

View File

@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
const Input = styled.input`
width: auto;
flex: 1;
width: auto;
flex: 1;
`;
export const LoginForm: React.VFC = () => {

View File

@ -63,7 +63,7 @@ export const NavigationMenu: React.VFC = () => {
{meta && (
<section>
<a className="item" href={CHANGELOG_URL} onClick={onClickItem}>
v{meta.version} {t('changelog')}
v{meta.version} {t('changelog')}
</a>
</section>
)}

View File

@ -3,19 +3,19 @@ import { useTranslation } from 'react-i18next';
import { $get } from '../misc/api';
interface RankingResponse {
isCalculating: boolean;
userCount: number;
ranking: Ranking[];
isCalculating: boolean;
userCount: number;
ranking: Ranking[];
}
interface Ranking {
username?: string;
host?: string;
rating: number;
username?: string;
host?: string;
rating: number;
}
export type RankingProps = {
limit?: number;
limit?: number;
};
export const Ranking: React.VFC<RankingProps> = ({limit}) => {
@ -60,7 +60,7 @@ export const Ranking: React.VFC<RankingProps> = ({limit}) => {
<div className="item flex" key={i}>
<div className="text-bold pr-2">{i + 1}</div>
<div>
-------------<br/>
-------------<br/>
<span className="text-dimmed text-75">{t('_missHai.rating')}: {r.rating}</span>
</div>
</div>

View File

@ -1,8 +1,8 @@
import React from 'react';
export type SkeletonProps = {
width?: string | number;
height?: string | number;
width?: string | number;
height?: string | number;
};
export const Skeleton: React.VFC<SkeletonProps> = (p) => {

View File

@ -1,15 +1,15 @@
import React from 'react';
export type TabItem = {
label: string;
key: string;
isNew?: boolean;
label: string;
key: string;
isNew?: boolean;
};
export type TabProps = {
items: TabItem[];
selected: string;
onSelect: (key: string) => void;
items: TabItem[];
selected: string;
onSelect: (key: string) => void;
};
// タブコンポーネント

View File

@ -50,7 +50,7 @@
"update": "Update",
"shareMisskeyTools": "Share #MisskeyTools",
"shareMisskeyToolsNote": "Try #MisskeyTools !\n\nhttps://t.psec.dev",
"instanceUrlPlaceholder": "e.g. misskey.io",
"instanceUrlPlaceholder": "k.lapy.link, stella.place, psec.dev...",
"settings": "Settings",
"accentColor": "Accent Color",
"changelog": "Changelog",
@ -156,7 +156,7 @@
}
},
"_error": {
"sorry": "Something went wrong. Please retry again.",
"sorry": "We have some small problem. Please try again later.",
"additionalInfo": "Additional Info: ",
"hitorisskeyIsDenied": "You cannot integrate with hitorisskey.",
"teapot": "I'm a teapot.",
@ -164,6 +164,7 @@
"tokenRequired": "Token is required.",
"invalidParameter": "Invalid parameter.",
"notAuthorized": "Not authorized.",
"noNewUserAllowed": "You cannot signup to this service for now.",
"hostNotFound": "Could not connect to the instance. Make sure that the host name is correct and the instance is live.",
"other": "None"
},

View File

@ -10,19 +10,19 @@ const merge = (baseData: Record<string, unknown>, newData: Record<string, unknow
});
};
const _enUS = merge(jaJP, enUS);
const _koKR = merge(_enUS, koKR)
const _enUS = merge(koKR, enUS);
const _jaJP = merge(_enUS, jaJP);
export const resources = {
'ja_JP': { translation: jaJP },
'ko_KR': { translation: koKR },
'en_US': { translation: _enUS },
'ko_KR': { translation: _koKR },
'ja_JP': { translation: _jaJP },
};
export const languageName = {
'ja_JP': '日本語',
'en_US': 'English',
'ko_KR': '한국어',
'en_US': 'English',
'ja_JP': '日本語',
} as const;
export type LanguageCode = keyof typeof resources;

View File

@ -50,7 +50,7 @@
"update": "更新する",
"shareMisskeyTools": "#MisskeyTools をシェアする",
"shareMisskeyToolsNote": "#MisskeyTools はいいぞ\n\nhttps://t.psec.dev",
"instanceUrlPlaceholder": "misskey.io",
"instanceUrlPlaceholder": "k.lapy.link, stella.place, psec.dev...",
"settings": "設定",
"accentColor": "アクセントカラー",
"changelog": "更新履歴",
@ -164,6 +164,7 @@
"tokenRequired": "トークンがありません。",
"invalidParameter": "パラメータが不正です。",
"notAuthorized": "権限がありません。",
"noNewUserAllowed": "現在、新規登録はできません。",
"hostNotFound": "インスタンスに接続できませんでした。ホスト名が正しく入力されているか、接続先のインスタンスが正常に動作しているかご確認ください。",
"other": "不明なエラーです。"
},

View File

@ -164,6 +164,7 @@
"tokenRequired": "잘못된 토큰이거나, 세션이 존재하지 않습니다.",
"invalidParameter": "잘못된 요청 내용입니다. 값을 확인 후 다시 시도하세요.",
"notAuthorized": "이 리소스에 접근할 권한이 없습니다.",
"noNewUserAllowed": "현재 본 서비스에 신규 가입하실 수 없습니다.",
"hostNotFound": "인스턴스에 접속할 수 없습니다. 입력한 인스턴스 주소가 올바른지, API 요청이 가능한지 확인해주세요.",
"other": "알 수 없는 오류가 발생했습니다. Mastodon 인스턴스로 로그인을 시도했을 수 있습니다."
},

View File

@ -43,7 +43,7 @@ export const AccountsPage: React.VFC = () => {
accounts.map(account => (
<button className="item fluid" style={{display: 'flex', flexDirection: 'row', alignItems: 'center'}} onClick={() => switchAccount(account.misshaiToken)}>
<i className="icon fas fa-chevron-right" />
@{account.username}@{account.host}
@{account.username}@{account.host}
<button className="btn flat text-danger" style={{marginLeft: 'auto'}} onClick={e => {
const filteredAccounts = accounts.filter(ac => ac.id !== account.id);
dispatch(setAccounts(filteredAccounts));

View File

@ -85,9 +85,9 @@ export const AdminPage: React.VFC = () => {
};
/**
* Session APIのエラーハンドリング
* APIがエラーを返した =
*/
* Session APIのエラーハンドリング
* APIがエラーを返した =
*/
useEffect(() => {
if (error) {
console.error(error);
@ -97,8 +97,8 @@ export const AdminPage: React.VFC = () => {
}, [error]);
/**
* Edit ModeがオンのときDelete Modeを無効化する
*/
* Edit ModeがオンのときDelete Modeを無効化する
*/
useEffect(() => {
if (isEditMode) {
setDeleteMode(false);
@ -106,8 +106,8 @@ export const AdminPage: React.VFC = () => {
}, [isEditMode]);
/**
*
*/
*
*/
useEffect(() => {
fetchAll();
}, []);
@ -166,7 +166,7 @@ export const AdminPage: React.VFC = () => {
{!isDeleteMode && (
<button className="item fluid" onClick={() => setEditMode(true)}>
<i className="icon fas fa-plus" />
</button>
)}
</div>
@ -174,22 +174,22 @@ export const AdminPage: React.VFC = () => {
) : (
<div className="vstack">
<label className="input-field">
<input type="text" value={draftTitle} onChange={e => setDraftTitle(e.target.value)} />
</label>
<label className="input-field">
<textarea className="input-field" value={draftBody} rows={10} onChange={e => setDraftBody(e.target.value)}/>
</label>
<div className="hstack" style={{justifyContent: 'flex-end'}}>
<button className="btn primary" onClick={submitAnnouncement} disabled={!draftTitle || !draftBody}>
</button>
<button className="btn" onClick={() => {
selectAnnouncement(null);
setEditMode(false);
}}>
</button>
</div>
</div>
@ -199,7 +199,7 @@ export const AdminPage: React.VFC = () => {
<h2> </h2>
<div className="vstack">
<button className="btn danger" onClick={onClickStartMisshaiAlertWorkerButton}>
Misskey Tools
Misskey Tools
</button>
<h3> </h3>
{misshaiLog && <LogView log={misshaiLog} />}

View File

@ -31,13 +31,13 @@ const variables = [
] as const;
type SettingDraftType = Partial<Pick<IUser,
| 'alertMode'
| 'visibility'
| 'localOnly'
| 'remoteFollowersOnly'
| 'template'
| 'useRanking'
| 'appendHashtag'
| 'alertMode'
| 'visibility'
| 'localOnly'
| 'remoteFollowersOnly'
| 'template'
| 'useRanking'
| 'appendHashtag'
>>;
type DraftReducer = React.Reducer<SettingDraftType, Partial<SettingDraftType>>;
@ -169,9 +169,9 @@ export const MisshaiPage: React.VFC = () => {
}, [dispatch, t]);
/**
* Session APIのエラーハンドリング
* APIがエラーを返した =
*/
* Session APIのエラーハンドリング
* APIがエラーを返した =
*/
useEffect(() => {
if (session.error) {
console.error(session.error);

View File

@ -51,7 +51,7 @@ export const IndexSessionPage: React.VFC = () => {
<td>{score.data?.followersCount ?? '...'}</td>
<td>{score.data?.followersDelta ?? '...'}</td>
</tr>
<tr>
<tr>
<td>{t('_missHai.rating')}</td>
<td>{session?.rating ?? '...'}</td>
</tr>
@ -64,7 +64,7 @@ export const IndexSessionPage: React.VFC = () => {
<div className="menu large">
<a className="item" href="https://psec.dev/@PSEC" target="_blank" rel="noopener noreferrer">
<i className="icon fas fa-at"></i>
@PSEC@psec.dev
@PSEC@psec.dev
</a>
</div>
</section>

View File

@ -10,60 +10,60 @@ import Twemoji from 'react-twemoji';
import { useAnnouncements } from '../hooks/useAnnouncements';
const Hero = styled.div<IsMobileProp>`
display: flex;
position: relative;
background: linear-gradient(-135deg, rgb(1, 169, 46), rgb(134, 179, 0) 35%);
color: var(--white);
padding: ${f => f.isMobile ? '16px' : '60px 90px'};
overflow: hidden;
gap: var(--margin);
> .hero {
flex: 2;
min-width: 0;
position: relative;
z-index: 1000;
p {
${f => f.isMobile ? 'font-size: 1rem;' : ''}
}
}
> .announcements {
flex: 1;
min-width: 0;
max-height: 512px;
overflow: auto;
padding: var(--margin);
border-radius: var(--radius);
background: var(--black-50);
backdrop-filter: blur(4px) brightness(120%);
z-index: 1000;
@media screen and (max-width: 800px) {
display: none;
}
}
> .rects {
position: absolute;
display: grid;
right: 160px;
bottom: -120px;
width: 400px;
height: 400px;
gap: 8px;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
transform-origin: center center;
transform: rotate(45deg);
opacity: 0.5;
> .rect {
border: 2px solid var(--white);
border-radius: 24px;
box-shadow: 0 2px 4px var(--shadow-color);
}
}
display: flex;
position: relative;
background: linear-gradient(-135deg, rgb(1, 169, 46), rgb(134, 179, 0) 35%);
color: var(--white);
padding: ${f => f.isMobile ? '16px' : '60px 90px'};
overflow: hidden;
gap: var(--margin);
> .hero {
flex: 2;
min-width: 0;
position: relative;
z-index: 1000;
p {
${f => f.isMobile ? 'font-size: 1rem;' : ''}
}
}
> .announcements {
flex: 1;
min-width: 0;
max-height: 512px;
overflow: auto;
padding: var(--margin);
border-radius: var(--radius);
background: var(--black-50);
backdrop-filter: blur(4px) brightness(120%);
z-index: 1000;
@media screen and (max-width: 800px) {
display: none;
}
}
> .rects {
position: absolute;
display: grid;
right: 160px;
bottom: -120px;
width: 400px;
height: 400px;
gap: 8px;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
transform-origin: center center;
transform: rotate(45deg);
opacity: 0.5;
> .rect {
border: 2px solid var(--white);
border-radius: 24px;
box-shadow: 0 2px 4px var(--shadow-color);
}
}
`;
const FormWrapper = styled.div`
max-width: 500px;
color: var(--fg);
max-width: 500px;
color: var(--fg);
`;
export const IndexWelcomePage: React.VFC = () => {
@ -102,7 +102,7 @@ export const IndexWelcomePage: React.VFC = () => {
</Hero>
<Twemoji options={{className: 'twemoji'}}>
<div className="py-4 text-125 text-center">
👍&emsp;&emsp;😆&emsp;🎉&emsp;🍮
👍&emsp;&emsp;😆&emsp;🎉&emsp;🍮
</div>
</Twemoji>
<article className="xarticle vstack pa-2">

View File

@ -14,23 +14,23 @@ import { designSystemColors } from '../../common/types/design-system-color';
import styled from 'styled-components';
const ColorInput = styled.input<{color: string}>`
display: block;
appearance: none;
width: 32px;
height: 32px;
border-radius: 999px;
background-color: var(--panel);
border: 4px solid var(--${p => p.color});
cursor: pointer;
transition: all 0.2s ease;
&:checked {
background: var(--${p => p.color});
cursor: default;
}
&:hover, &:focus {
box-shadow: 0 0 16px var(--${p => p.color});
outline: none;
}
display: block;
appearance: none;
width: 32px;
height: 32px;
border-radius: 999px;
background-color: var(--panel);
border: 4px solid var(--${p => p.color});
cursor: pointer;
transition: all 0.2s ease;
&:checked {
background: var(--${p => p.color});
cursor: default;
}
&:hover, &:focus {
box-shadow: 0 0 16px var(--${p => p.color});
outline: none;
}
`;
export const SettingPage: React.VFC = () => {

View File

@ -1,13 +1,13 @@
@font-face {
font-family: 'Pretendard JP Variable';
font-weight: 45 920;
font-style: normal;
font-display: swap;
src: url("/assets/PretendardJPVariable.woff2") format('woff2-variations');
font-family: 'Pretendard JP Variable';
font-weight: 45 920;
font-style: normal;
font-display: swap;
src: url("/assets/PretendardJPVariable.woff2") format('woff2-variations');
}
html {
font-size: 16px;
font-size: 16px;
}
body {

View File

@ -1,9 +1,9 @@
@font-face {
font-family: 'Pretendard JP Variable';
font-weight: 45 920;
font-style: normal;
font-display: swap;
src: url("/assets/PretendardJPVariable.woff2") format('woff2-variations');
font-family: 'Pretendard JP Variable';
font-weight: 45 920;
font-style: normal;
font-display: swap;
src: url("/assets/PretendardJPVariable.woff2") format('woff2-variations');
}
@font-face {

View File

@ -8,74 +8,74 @@ const isProduction = process.env.NODE_ENV === 'production';
const meta = require('./package.json');
module.exports = {
entry: {
fe: './src/frontend/init.tsx',
},
module: {
rules: [{
test: /\.(eot|woff|woff2|svg|ttf)([?]?.*)$/,
type: 'asset/resource'
}, {
test: /\.json5$/,
loader: 'json5-loader',
options: {
esModule: false,
},
type: 'javascript/auto'
}, {
test: /\.tsx?$/,
use: [
{ loader: 'ts-loader' }
]
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
url: false,
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
sassOptions: {
fiber: false
},
sourceMap: true,
},
},
],
}, {
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}]
},
plugins: [
new webpack.ProgressPlugin({}),
],
output: {
path: __dirname + '/built/assets',
filename: `[name].${meta.version}.js`,
publicPath: '/assets/',
pathinfo: false,
},
resolve: {
extensions: [
'.js', '.ts', '.json', '.tsx'
],
alias: {
}
},
resolveLoader: {
modules: ['node_modules']
},
experiments: {
topLevelAwait: true
},
devtool: false, //'source-map',
mode: isProduction ? 'production' : 'development'
entry: {
fe: './src/frontend/init.tsx',
},
module: {
rules: [{
test: /\.(eot|woff|woff2|svg|ttf)([?]?.*)$/,
type: 'asset/resource'
}, {
test: /\.json5$/,
loader: 'json5-loader',
options: {
esModule: false,
},
type: 'javascript/auto'
}, {
test: /\.tsx?$/,
use: [
{ loader: 'ts-loader' }
]
}, {
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
url: false,
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
sassOptions: {
fiber: false
},
sourceMap: true,
},
},
],
}, {
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}]
},
plugins: [
new webpack.ProgressPlugin({}),
],
output: {
path: __dirname + '/built/assets',
filename: `[name].${meta.version}.js`,
publicPath: '/assets/',
pathinfo: false,
},
resolve: {
extensions: [
'.js', '.ts', '.json', '.tsx'
],
alias: {
}
},
resolveLoader: {
modules: ['node_modules']
},
experiments: {
topLevelAwait: true
},
devtool: false, //'source-map',
mode: isProduction ? 'production' : 'development'
};