initial commit
This commit is contained in:
131
.gitignore
vendored
Normal file
131
.gitignore
vendored
Normal file
@@ -0,0 +1,131 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
WORKDIR /work
|
||||
COPY . /work
|
||||
|
||||
RUN npm ci
|
||||
|
||||
EXPOSE 3000
|
||||
ENTRYPOINT ["/usr/bin/env"]
|
||||
CMD ["npm", "start"]
|
||||
29
k8s/deploy.amane-frontend.yaml
Normal file
29
k8s/deploy.amane-frontend.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: amane-frontend
|
||||
namespace: amane-tanikaze
|
||||
labels:
|
||||
app: amane-frontend
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: amane-frontend
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: amane-frontend
|
||||
spec:
|
||||
containers:
|
||||
- name: dcbot
|
||||
image: 'gitea.konchin.com/services/amane-tanikaze-frontend:latest'
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 3000
|
||||
env:
|
||||
- name: WDS_SOCKET_PORT
|
||||
value: 0
|
||||
imagePullSecrets:
|
||||
- name: regcred
|
||||
16
k8s/svc.amane-frontend.yaml
Normal file
16
k8s/svc.amane-frontend.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
namespace: amane-tanikaze
|
||||
name: amane-frontend
|
||||
labels:
|
||||
app: amane-frontend
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: amane-frontend
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: 3000
|
||||
18042
package-lock.json
generated
Normal file
18042
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.70",
|
||||
"@types/react": "^18.2.47",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"react": "^18.2.0",
|
||||
"react-cookie": "^7.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
43
public/index.html
Normal file
43
public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/logo/poop.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo/poop.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Autoreact Image</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
public/logo/amane.png
Normal file
BIN
public/logo/amane.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 351 KiB |
BIN
public/logo/poop.png
Normal file
BIN
public/logo/poop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
15
public/manifest.json
Normal file
15
public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "poop.png",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
7
public/svg/info.svg
Normal file
7
public/svg/info.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="160" width="160" version="1.0">
|
||||
<g fill="#4b4b4b">
|
||||
<path d="m80 15c-35.88 0-65 29.12-65 65s29.12 65 65 65 65-29.12 65-65-29.12-65-65-65zm0 10c30.36 0 55 24.64 55 55s-24.64 55-55 55-55-24.64-55-55 24.64-55 55-55z"/>
|
||||
<path d="m57.373 18.231a9.3834 9.1153 0 1 1 -18.767 0 9.3834 9.1153 0 1 1 18.767 0z" transform="matrix(1.1989 0 0 1.2342 21.214 28.75)"/>
|
||||
<path d="m90.665 110.96c-0.069 2.73 1.211 3.5 4.327 3.82l5.008 0.1v5.12h-39.073v-5.12l5.503-0.1c3.291-0.1 4.082-1.38 4.327-3.82v-30.813c0.035-4.879-6.296-4.113-10.757-3.968v-5.074l30.665-1.105"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 617 B |
3
public/svg/search.svg
Normal file
3
public/svg/search.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg id="tnb-google-search-icon" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.8153 10.3439C12.6061 9.2673 13.0732 7.9382 13.0732 6.5C13.0732 2.91015 10.163 0 6.57318 0C2.98333 0 0.0731812 2.91015 0.0731812 6.5C0.0731812 10.0899 2.98333 13 6.57318 13C8.01176 13 9.3412 12.5327 10.4179 11.7415L10.4171 11.7422C10.4466 11.7822 10.4794 11.8204 10.5156 11.8566L14.3661 15.7071C14.7566 16.0976 15.3898 16.0976 15.7803 15.7071C16.1708 15.3166 16.1708 14.6834 15.7803 14.2929L11.9298 10.4424C11.8936 10.4062 11.8553 10.3734 11.8153 10.3439ZM12.0732 6.5C12.0732 9.53757 9.61075 12 6.57318 12C3.53561 12 1.07318 9.53757 1.07318 6.5C1.07318 3.46243 3.53561 1 6.57318 1C9.61075 1 12.0732 3.46243 12.0732 6.5Z" fill="black"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 764 B |
3
src/config.ts
Normal file
3
src/config.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const config = {
|
||||
aliasPerPage: 10
|
||||
};
|
||||
0
src/css/.Rhistory
Normal file
0
src/css/.Rhistory
Normal file
47
src/css/banner.css
Normal file
47
src/css/banner.css
Normal file
@@ -0,0 +1,47 @@
|
||||
div#fixed-banner {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 10%;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
border-top: 0.8rem solid grey;
|
||||
border-bottom: 0.8rem solid grey;
|
||||
z-index: 1;
|
||||
}
|
||||
div.banner.container {
|
||||
flex-flow: row nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
div.banner.element {
|
||||
height: 100%;
|
||||
}
|
||||
div.banner.element.left {
|
||||
width: 10%;
|
||||
min-width: 5rem;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
div.banner.element.right {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
div.left-banner.container {
|
||||
flex-flow: row nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
img.left-banner.element {
|
||||
max-height: 80%;
|
||||
max-width: 80%;
|
||||
margin: 0 10px;
|
||||
}
|
||||
div.right-banner.container {
|
||||
flex-flow: row-reverse nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
div.right-banner.element {
|
||||
width: 100%;
|
||||
}
|
||||
60
src/css/content.css
Normal file
60
src/css/content.css
Normal file
@@ -0,0 +1,60 @@
|
||||
div#image-col {
|
||||
position: absolute;
|
||||
top: calc(10% + 1.6rem);
|
||||
left: min(15vw, 70px);
|
||||
width: calc(100% - min(15vw, 70px));
|
||||
height: calc(90% - 1.6rem);
|
||||
}
|
||||
div.image-row.container {
|
||||
flex-flow: column nowrap;
|
||||
align-items: start;
|
||||
width: calc(100% - 1rem);
|
||||
margin: 0.5rem;
|
||||
border: 0.1rem solid #cccccc;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
span.anchor {
|
||||
position: absolute;
|
||||
margin-top: calc(-10% - 2rem);
|
||||
}
|
||||
div.image-row.container:nth-child(odd) {
|
||||
background-color: #fefefe;
|
||||
}
|
||||
div.image-row.container:nth-child(even) {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
div.image-row.element {
|
||||
height: 100%;
|
||||
}
|
||||
h3.image-row.element {
|
||||
font-size: max(min(3vw, 1.2rem), 10px);
|
||||
margin: 0.5rem 1rem;
|
||||
}
|
||||
div.image.container {
|
||||
flex-flow: row wrap;
|
||||
height: 100%;
|
||||
}
|
||||
img.image.element {
|
||||
height: min(25vw, 7rem);
|
||||
max-width: 16rem;
|
||||
margin: 0 1rem;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
div.navigate.container {
|
||||
justify-content: space-around;
|
||||
width: 100%;
|
||||
}
|
||||
button.navigate.element {
|
||||
height: 2rem;
|
||||
min-width: 15vw;
|
||||
font-size: 1rem;
|
||||
padding: 0 12px;
|
||||
border: 1px solid grey;
|
||||
border-radius: 5px;
|
||||
margin: 5px 10px;
|
||||
background-color: #eeeeee;
|
||||
}
|
||||
button.navigate.element:hover {
|
||||
background-color: #cccccc;
|
||||
border: 1px solid grey;
|
||||
}
|
||||
26
src/css/general.css
Normal file
26
src/css/general.css
Normal file
@@ -0,0 +1,26 @@
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
body {
|
||||
background-color: white;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: black;
|
||||
}
|
||||
code {
|
||||
font-family: consolas, monospace;
|
||||
}
|
||||
div.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
div.leftright.container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
div.leftright.element {
|
||||
height: 100%;
|
||||
}
|
||||
div#image-col {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
49
src/css/search-bar.css
Normal file
49
src/css/search-bar.css
Normal file
@@ -0,0 +1,49 @@
|
||||
.search-bar.container {
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
width: calc(100% - 2 * min(4vw, 20px));
|
||||
padding: 5px min(4vw, 20px);
|
||||
height: 30px;
|
||||
}
|
||||
.search-bar.element {
|
||||
height: 100%;
|
||||
}
|
||||
.search-bar.element:has(input) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
div#search-bar {
|
||||
position: relative;
|
||||
width: 65%;
|
||||
height: 30px;
|
||||
border: none;
|
||||
outline: 0;
|
||||
}
|
||||
input[type="text"] {
|
||||
border: 0.5px solid grey;
|
||||
border-radius: 25px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
text-rendering: auto;
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
text-transform: none;
|
||||
text-indent: 12px;
|
||||
text-shadow: none;
|
||||
display: inline-block;
|
||||
text-align: start;
|
||||
appearance: auto;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border: 1px solid grey;
|
||||
}
|
||||
|
||||
div.search-bar > img {
|
||||
position: relative;
|
||||
left: -30px;
|
||||
height: 60%;
|
||||
padding: 25% 0 15% 0;
|
||||
}
|
||||
|
||||
59
src/css/sidebar.css
Normal file
59
src/css/sidebar.css
Normal file
@@ -0,0 +1,59 @@
|
||||
div.fixed-sidebar {
|
||||
position: fixed;
|
||||
top: calc(10% + 1.6rem);
|
||||
left: 0;
|
||||
height: calc(90% - 1.6rem);
|
||||
width: min(15vw, 70px);
|
||||
background-color: #eeeeee;
|
||||
z-index: 1;
|
||||
}
|
||||
div.fixed-sidebar:hover {
|
||||
width: auto;
|
||||
}
|
||||
div.sidebar.container {
|
||||
flex-flow: column nowrap;
|
||||
align-items: start;
|
||||
height: 100%;
|
||||
}
|
||||
div.sidebar.element {
|
||||
width: 100%;
|
||||
}
|
||||
div.alias.container {
|
||||
flex-flow: column nowrap;
|
||||
width: 100%;
|
||||
align-items: start;
|
||||
}
|
||||
div.sidebar.element:has(h2#alias-title) {
|
||||
height: 2.5rem;
|
||||
border-bottom: 0.5px solid black;
|
||||
}
|
||||
h2#alias-title {
|
||||
margin: min(2vw, 10px);
|
||||
font-size: min(5vw, 18px);
|
||||
}
|
||||
div.sidebar.element:has(div.alias.container) {
|
||||
height: calc(100% - 2.5rem - 1px);
|
||||
}
|
||||
div.alias.container {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
button.alias.element {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: black;
|
||||
white-space: nowrap;
|
||||
font-size: min(4vw, 16px);
|
||||
text-decoration: none;
|
||||
text-align: start;
|
||||
padding: min(1.5vw, 7.5px) min(2vw, 10px);
|
||||
border: 0;
|
||||
}
|
||||
button.alias.element:hover {
|
||||
color: white;
|
||||
background-color: #333333;
|
||||
}
|
||||
button.alias.element.highlight {
|
||||
background-color: #cccccc;
|
||||
}
|
||||
40
src/index.tsx
Normal file
40
src/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import {BrowserRouter, Routes, Route} from 'react-router-dom';
|
||||
|
||||
import Index from './pages/root/Index';
|
||||
import Error403 from './pages/error/Error403';
|
||||
import Error404 from './pages/error/Error404';
|
||||
|
||||
import './css/general.css';
|
||||
import './css/banner.css';
|
||||
import './css/search-bar.css';
|
||||
import './css/sidebar.css';
|
||||
import './css/content.css';
|
||||
|
||||
function App(){
|
||||
return <BrowserRouter>
|
||||
<Routes>
|
||||
<Route path='/'>
|
||||
<Route index element={<Index />} />
|
||||
<Route path='error403' element={<Error403 />} />
|
||||
<Route path='error404' element={<Error404 />} />
|
||||
<Route path='*' element={<Error404 />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
}
|
||||
|
||||
function main(){
|
||||
const element: Element | null = document.getElementById('root');
|
||||
if(!element) throw Error('element not exist');
|
||||
const root = ReactDOM.createRoot(element);
|
||||
if(!root) throw Error('root not exist');
|
||||
root.render(<App />);
|
||||
}
|
||||
|
||||
try{main();}catch(err: unknown){
|
||||
if(err instanceof Error)
|
||||
console.log(err.message);
|
||||
else
|
||||
console.log(String(err));
|
||||
}
|
||||
6
src/models/Alias.ts
Normal file
6
src/models/Alias.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Alias{
|
||||
_id: string;
|
||||
guild: string;
|
||||
text: string;
|
||||
images: string[];
|
||||
}
|
||||
9
src/models/AliasAPI.ts
Normal file
9
src/models/AliasAPI.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ImageAPI{
|
||||
id: string;
|
||||
ext: string;
|
||||
}
|
||||
export interface AliasAPI{
|
||||
text: string;
|
||||
images: ImageAPI[];
|
||||
}
|
||||
|
||||
6
src/models/Guild.ts
Normal file
6
src/models/Guild.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface Guild{
|
||||
_id: string;
|
||||
guildId: string;
|
||||
giveawayLogChannelId: string;
|
||||
autoroleLogChannelId: string;
|
||||
}
|
||||
4
src/models/Image.ts
Normal file
4
src/models/Image.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface Image{
|
||||
_id: string; // act as random generated filename
|
||||
extension: string;
|
||||
}
|
||||
9
src/pages/error/Error403.tsx
Normal file
9
src/pages/error/Error403.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
function Error403(){
|
||||
return <div>
|
||||
<h1 style={{textAlign: 'center'}}>403 Forbidden</h1>
|
||||
<p style={{textAlign: 'center'}}>You don't have the permission to access this page.</p>
|
||||
<p style={{textAlign: 'center'}}>Or your token had been expired.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Error403;
|
||||
8
src/pages/error/Error404.tsx
Normal file
8
src/pages/error/Error404.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
function Error404(){
|
||||
return <div>
|
||||
<h1 style={{textAlign: 'center'}}>404 Not Found</h1>
|
||||
<p style={{textAlign: 'center'}}>The required page does not exist.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default Error404;
|
||||
24
src/pages/root/Banner.tsx
Normal file
24
src/pages/root/Banner.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import {Dispatch} from 'react';
|
||||
|
||||
import SearchBar, {SearchBarProps} from './SearchBar';
|
||||
|
||||
function Banner(props: SearchBarProps){
|
||||
return <div id="fixed-banner" className="banner">
|
||||
<div className="banner container">
|
||||
<div className="banner element left">
|
||||
<div className="left-banner container">
|
||||
<img id="banner-icon" className="left-banner element" src="logo/amane.png"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="banner element right">
|
||||
<div className="right-banner container">
|
||||
<div className="right-banner element">
|
||||
<SearchBar setInput={props.setInput}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default Banner;
|
||||
94
src/pages/root/Content.tsx
Normal file
94
src/pages/root/Content.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import {useEffect, useState, useRef, Dispatch} from 'react';
|
||||
|
||||
import {AliasAPI, ImageAPI} from '../../models/AliasAPI';
|
||||
import {Image} from '../../models/Image';
|
||||
import {config} from '../../config';
|
||||
|
||||
interface NavigateBarProps{
|
||||
aliases: AliasAPI[];
|
||||
page: number;
|
||||
setPage: Dispatch<number>;
|
||||
}
|
||||
|
||||
interface ContentProps{
|
||||
aliases: AliasAPI[];
|
||||
page: number;
|
||||
setPage: Dispatch<number>;
|
||||
scroll: number;
|
||||
setScroll: Dispatch<number>;
|
||||
}
|
||||
|
||||
function NavigateBar(props: NavigateBarProps){
|
||||
return <div className="navigate container">
|
||||
<button className="navigate element"
|
||||
onClick={() => {
|
||||
props.setPage(Math.max(0, props.page-1));
|
||||
}}>Prev</button>
|
||||
<p>{props.page}</p>
|
||||
<button className="navigate element"
|
||||
onClick={() => {
|
||||
props.setPage(Math.min(Math.ceil(
|
||||
props.aliases.length / config.aliasPerPage) - 1,
|
||||
props.page+1));
|
||||
}}>Next</button>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Content(props: ContentProps){
|
||||
const [content, setContent] = useState(Array<any>());
|
||||
const [scroll, setScroll] = useState(-1);
|
||||
const ref = useRef(new Array(props.aliases.length));
|
||||
|
||||
const handlePage = () => {
|
||||
const newContent: Array<any> = [];
|
||||
for(const i in props.aliases.slice(
|
||||
props.page * config.aliasPerPage,
|
||||
(props.page + 1) * config.aliasPerPage
|
||||
)){
|
||||
const j: number = Number(i) + props.page * config.aliasPerPage;
|
||||
const a: AliasAPI = props.aliases[j];
|
||||
newContent.push(
|
||||
<div key={`${a.text}-image-row`}
|
||||
className="image-row container">
|
||||
<span ref={el => ref.current[j] = el} className="anchor"></span>
|
||||
<h3 className="image-row element">{a.text}</h3>
|
||||
<div className="image-row element">
|
||||
<div className="image container">
|
||||
{a.images.map((img: ImageAPI) =>
|
||||
<img key={`${a.text}-${img.id}`}
|
||||
className="image element"
|
||||
src={`/img/${img.id}${img.ext}`}
|
||||
title={img.id+img.ext}/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
setContent(newContent);
|
||||
};
|
||||
useEffect(handlePage, [props.aliases, props.page]);
|
||||
|
||||
const handleScroll = () => {
|
||||
if(props.scroll === -1){
|
||||
if(ref.current[scroll])
|
||||
ref.current[scroll].scrollIntoView({behavior: 'smooth'});
|
||||
return;
|
||||
}
|
||||
if(ref.current[props.scroll])
|
||||
ref.current[props.scroll].scrollIntoView({behavior: 'smooth'});
|
||||
setScroll(props.scroll);
|
||||
props.setScroll(-1);
|
||||
};
|
||||
useEffect(handleScroll, [content, props.scroll]);
|
||||
|
||||
return <div id="image-col" className="leftright element">
|
||||
<NavigateBar aliases={props.aliases}
|
||||
page={props.page} setPage={props.setPage}/>
|
||||
{content}
|
||||
<NavigateBar aliases={props.aliases}
|
||||
page={props.page} setPage={props.setPage}/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default Content;
|
||||
74
src/pages/root/Index.tsx
Normal file
74
src/pages/root/Index.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import {useEffect, useState} from 'react';
|
||||
import {Navigate} from 'react-router-dom';
|
||||
|
||||
import {AliasAPI} from '../../models/AliasAPI';
|
||||
|
||||
import Banner from './Banner';
|
||||
import Sidebar from './Sidebar';
|
||||
import Content from './Content';
|
||||
|
||||
function Index(){
|
||||
const [aliases, setAliases] =
|
||||
useState(Array<AliasAPI>());
|
||||
const [filtered, setFiltered] =
|
||||
useState(Array<AliasAPI>());
|
||||
const [input, setInput] = useState('');
|
||||
const [status, setStatus] = useState(200);
|
||||
const [page, setPage] = useState(0);
|
||||
const [scroll, setScroll] = useState(-1);
|
||||
|
||||
const handleFetch = async () => {
|
||||
try{
|
||||
const res = await fetch('/api');
|
||||
if(res.status !== 200)
|
||||
setStatus(res.status);
|
||||
setAliases(await res.json());
|
||||
}catch(err: unknown){
|
||||
if(err instanceof Error)
|
||||
console.error(`Error, ${err.message}`);
|
||||
else console.error(`Error, ${String(err)}`);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
handleFetch().catch(err=>console.error(err))
|
||||
}, []);
|
||||
|
||||
const handleFilter = async () => {
|
||||
try{
|
||||
const res: Array<AliasAPI> = [];
|
||||
for(const i of aliases)
|
||||
if(i.text.startsWith(input))
|
||||
res.push(i)
|
||||
setFiltered(res);
|
||||
setPage(0);
|
||||
}catch(err: unknown){
|
||||
if(err instanceof Error)
|
||||
console.error(`Error, ${err.message}`);
|
||||
else console.error(`Error, ${String(err)}`);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
handleFilter().catch(err=>console.error(err))
|
||||
}, [aliases, input]);
|
||||
|
||||
if(status !== 200)
|
||||
return <Navigate to={`/error${status}`} />;
|
||||
|
||||
return <>
|
||||
<Banner setInput={setInput}/>
|
||||
<div className="leftright container">
|
||||
<Sidebar
|
||||
aliases={filtered}
|
||||
page={page} setPage={setPage}
|
||||
setScroll={setScroll}
|
||||
/>
|
||||
<Content
|
||||
aliases={filtered}
|
||||
page={page} setPage={setPage}
|
||||
scroll={scroll} setScroll={setScroll}
|
||||
/>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
export default Index;
|
||||
33
src/pages/root/SearchBar.tsx
Normal file
33
src/pages/root/SearchBar.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import {useEffect, useState, KeyboardEvent, Dispatch} from 'react';
|
||||
|
||||
export interface SearchBarProps{
|
||||
setInput: Dispatch<string>;
|
||||
};
|
||||
|
||||
function SearchBar(props: SearchBarProps){
|
||||
const handleInput = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if(e.target instanceof Element && e.key === "Enter"){
|
||||
props.setInput((e.target as HTMLInputElement).value);
|
||||
}
|
||||
};
|
||||
|
||||
// useEffect(()=>{}, [props.input]);
|
||||
return <div className="search-bar container">
|
||||
<div className="search-bar element">
|
||||
<input id="search-bar" type="text"
|
||||
placeholder="Search for aliases..."
|
||||
autoComplete="off"
|
||||
onKeyDown={handleInput}
|
||||
/>
|
||||
</div>
|
||||
<div className="search-bar element">
|
||||
<img
|
||||
id="search-icon"
|
||||
src="svg/search.svg"
|
||||
alt="search-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default SearchBar;
|
||||
60
src/pages/root/Sidebar.tsx
Normal file
60
src/pages/root/Sidebar.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import {useEffect, useState, Dispatch, useRef} from 'react';
|
||||
|
||||
import {AliasAPI} from '../../models/AliasAPI';
|
||||
import {config} from '../../config';
|
||||
|
||||
interface SidebarProps{
|
||||
aliases: AliasAPI[];
|
||||
page: number;
|
||||
setPage: Dispatch<number>;
|
||||
setScroll: Dispatch<number>;
|
||||
}
|
||||
|
||||
function Sidebar(props: SidebarProps){
|
||||
const [content, setContent] = useState(Array<any>());
|
||||
|
||||
const pageL: number = props.page * config.aliasPerPage;
|
||||
const pageR: number = pageL + config.aliasPerPage;
|
||||
|
||||
const handleClick = (idx: number) => {
|
||||
props.setPage(Math.floor(idx / config.aliasPerPage));
|
||||
props.setScroll(idx);
|
||||
};
|
||||
|
||||
const handlePage = async () => {
|
||||
const newContent: Array<any> = [];
|
||||
for(const i in props.aliases){
|
||||
const alias: AliasAPI = props.aliases[i];
|
||||
newContent.push(
|
||||
<button key={`${alias.text}-sidebar`}
|
||||
className={`alias element${
|
||||
pageL <= Number(i) && Number(i) < pageR ?
|
||||
' highlight' : ''}`
|
||||
} title={alias.text}
|
||||
onClick={() => handleClick(Number(i))}>
|
||||
{alias.text}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
setContent(newContent);
|
||||
};
|
||||
useEffect(() => {handlePage().catch(
|
||||
err => console.error(err))}, [props.aliases, props.page]);
|
||||
|
||||
return <div id="sidbar-col" className="leftright element">
|
||||
<div className="fixed-sidebar">
|
||||
<div className="sidebar container">
|
||||
<div className="sidebar element">
|
||||
<h2 id="alias-title">Alias</h2>
|
||||
</div>
|
||||
<div className="sidebar element">
|
||||
<div className="alias container">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
26
tsconfig.json
Normal file
26
tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user