init repo
This commit is contained in:
commit
f8f94217ee
|
@ -0,0 +1,6 @@
|
||||||
|
APP_DEBUG=true
|
||||||
|
|
||||||
|
DB_PASS=db
|
||||||
|
DB_NAME=db
|
||||||
|
DB_USER=db
|
||||||
|
DB_HOST=db
|
|
@ -0,0 +1,5 @@
|
||||||
|
/docker-compose.yml
|
||||||
|
/.env
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
|
@ -0,0 +1,7 @@
|
||||||
|
# digital.solutions.test
|
||||||
|
to start up
|
||||||
|
|
||||||
|
```
|
||||||
|
cp docker-compose.yml.dev docker-compose.yml
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "node",
|
||||||
|
"request": "attach",
|
||||||
|
"name": "Attach to docker",
|
||||||
|
"restart": true,
|
||||||
|
"localRoot": ".",
|
||||||
|
"remoteRoot": "/app",
|
||||||
|
"address": "localhost",
|
||||||
|
"port": 9229
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "back",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/express-session": "^1.18.0",
|
||||||
|
"@types/node": "^22.9.0",
|
||||||
|
"esbuild": "^0.24.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/build.js",
|
||||||
|
"start": "yarn build && node dist/index.js",
|
||||||
|
"typeorm": "yarn build && typeorm -d dist/typeorm/data-source.repo.js",
|
||||||
|
"docker": "docker-compose exec back yarn"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"connect-typeorm": "^2.0.0",
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"express": "^4.21.1",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
|
"pg": "^8.13.1",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"typeorm": "^0.3.20"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import esbuild from 'esbuild';
|
||||||
|
import fsp from 'node:fs/promises';
|
||||||
|
|
||||||
|
fsp.rm('dist', { recursive: true, force: true });
|
||||||
|
fsp.mkdir('dist', { recursive: true });
|
||||||
|
const src = await fsp.readdir('src', { recursive: true });
|
||||||
|
const filtered = src.filter(x => x.endsWith('.ts')).map(x => 'src/' + x);
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: filtered,
|
||||||
|
outdir: 'dist',
|
||||||
|
tsconfig: 'tsconfig.json'
|
||||||
|
});
|
|
@ -0,0 +1,8 @@
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config({
|
||||||
|
path: [
|
||||||
|
'/.env.global',
|
||||||
|
'/app/.env'
|
||||||
|
]
|
||||||
|
})
|
|
@ -0,0 +1,36 @@
|
||||||
|
import express from 'express';
|
||||||
|
import session from 'express-session';
|
||||||
|
import 'reflect-metadata';
|
||||||
|
|
||||||
|
import routes from './routes.js';
|
||||||
|
import dataSourceRepo from './typeorm/data-source.repo.js';
|
||||||
|
|
||||||
|
import './env.js';
|
||||||
|
import { TypeORMError } from 'typeorm';
|
||||||
|
import { TypeormStore } from 'connect-typeorm';
|
||||||
|
|
||||||
|
const listen_host = process.env.LISTEN_HOST ?? '0.0.0.0';
|
||||||
|
const listen_port = parseInt(process.env.LISTEN_PORT ?? '80');
|
||||||
|
|
||||||
|
await dataSourceRepo.initialize();
|
||||||
|
await dataSourceRepo.runMigrations();
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(session({
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
store: new TypeormStore({
|
||||||
|
cleanupLimit: 2,
|
||||||
|
limitSubquery: false,
|
||||||
|
ttl: 3600
|
||||||
|
}),
|
||||||
|
secret: 'keyboard cat'
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use('/api', routes());
|
||||||
|
|
||||||
|
app.listen(listen_port, listen_host, () => {
|
||||||
|
console.log(`Listening on ${listen_host}:${listen_port}`)
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
|
||||||
|
import data from './routes/data.js';
|
||||||
|
import selection from './routes/selection.js';
|
||||||
|
|
||||||
|
export default function(): Router {
|
||||||
|
const app = Router();
|
||||||
|
app.use('/data', data());
|
||||||
|
app.use('/selection', selection())
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { IRouter, Router } from 'express';
|
||||||
|
import dataSourceRepo from '../typeorm/data-source.repo.js';
|
||||||
|
import { DataRow } from '../typeorm/entity/DataRow.entity.js';
|
||||||
|
|
||||||
|
export default function(): IRouter {
|
||||||
|
const app = Router();
|
||||||
|
app.get('/list', async (req, res) => {
|
||||||
|
const [page, pageSz, search] = [
|
||||||
|
parseInt(req.query.page as string | undefined),
|
||||||
|
parseInt((req.query.page_sz ?? '20').toString()),
|
||||||
|
req.query.search as string | undefined
|
||||||
|
];
|
||||||
|
if (pageSz <= 0) {
|
||||||
|
res.status(400);
|
||||||
|
res.send('invalid page_sz');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = dataSourceRepo.getRepository(DataRow).createQueryBuilder('data');
|
||||||
|
if (!Number.isNaN(page)) {
|
||||||
|
query.take(pageSz);
|
||||||
|
query.skip(page * pageSz);
|
||||||
|
}
|
||||||
|
if (search !== undefined) {
|
||||||
|
query.where('data like :search', { search });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await query.getMany();
|
||||||
|
res.send(data);
|
||||||
|
})
|
||||||
|
|
||||||
|
app.put('/', async (req, res) => {
|
||||||
|
const data = req.body.data;
|
||||||
|
if (!data) {
|
||||||
|
res.send(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = new DataRow();
|
||||||
|
row.data = data;
|
||||||
|
await dataSourceRepo.getRepository(DataRow).insert(row);
|
||||||
|
|
||||||
|
res.status(200);
|
||||||
|
res.send('');
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { IRouter, Router } from "express";
|
||||||
|
|
||||||
|
export default function(): IRouter {
|
||||||
|
const app = Router();
|
||||||
|
return app;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { DataSource } from "typeorm";
|
||||||
|
import '../env.js';
|
||||||
|
|
||||||
|
export default new DataSource({
|
||||||
|
type: 'postgres',
|
||||||
|
host: process.env.DB_HOST,
|
||||||
|
port: 5432,
|
||||||
|
username: process.env.DB_USER,
|
||||||
|
password: process.env.DB_PASS,
|
||||||
|
database: process.env.DB_NAME,
|
||||||
|
entities: [ import.meta.dirname + "/entity/*.js" ],
|
||||||
|
migrations: [ import.meta.dirname + "/migration/*.js" ],
|
||||||
|
logging: true,
|
||||||
|
synchronize: true
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import 'reflect-metadata';
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class DataRow {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column('varchar')
|
||||||
|
data: string
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Column, DeleteDateColumn, Entity, Index, PrimaryColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Session {
|
||||||
|
@Index()
|
||||||
|
@Column('bigint')
|
||||||
|
public expiredAt = Date.now();
|
||||||
|
|
||||||
|
@PrimaryColumn('varchar', { length: 255 })
|
||||||
|
public id = '';
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
public json = '';
|
||||||
|
|
||||||
|
@DeleteDateColumn()
|
||||||
|
public destroyedAt?: Date;
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { MigrationInterface, QueryRunner, Table, TableColumn } from "typeorm";
|
||||||
|
|
||||||
|
export class Datarow1731074164993 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'DataRow',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'bigint',
|
||||||
|
isGenerated: true,
|
||||||
|
isPrimary: true,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'data',
|
||||||
|
type: 'varchar',
|
||||||
|
isNullable: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
queryRunner.dropTable('DataRow', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { MigrationInterface, QueryRunner, Table, TableColumn } from "typeorm";
|
||||||
|
|
||||||
|
export class Session1731074164994 implements MigrationInterface {
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: 'Session',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'expiredAt',
|
||||||
|
type: 'bigint',
|
||||||
|
isGenerated: true,
|
||||||
|
isPrimary: true,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
type: 'varchar',
|
||||||
|
length: '255',
|
||||||
|
isNullable: false,
|
||||||
|
isUnique: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'json',
|
||||||
|
type: 'varchar',
|
||||||
|
isNullable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'destroyedAt',
|
||||||
|
type: 'date',
|
||||||
|
isNullable: true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
queryRunner.dropTable('Session', true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
yarn
|
||||||
|
yarn build
|
||||||
|
node dist/index.js --inspect=0.0.0.0:9229
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["es5", "es6", "dom"],
|
||||||
|
"target": "es2017",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
||||||
|
:80 {
|
||||||
|
route /api/* {
|
||||||
|
reverse_proxy http://back
|
||||||
|
}
|
||||||
|
reverse_proxy http://front
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
services:
|
||||||
|
back:
|
||||||
|
image: node:22-alpine3.20
|
||||||
|
# restart: always
|
||||||
|
ports:
|
||||||
|
- 9229:9229
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
aliases:
|
||||||
|
- back
|
||||||
|
entrypoint: '/app/start.sh'
|
||||||
|
working_dir: '/app'
|
||||||
|
volumes:
|
||||||
|
- './back:/app'
|
||||||
|
- 'back-node-modules:/app/node_modules'
|
||||||
|
- './.env:/.env.global:ro'
|
||||||
|
front:
|
||||||
|
build:
|
||||||
|
context: front
|
||||||
|
dockerfile: Dockerfile.dev
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
aliases:
|
||||||
|
- front
|
||||||
|
volumes:
|
||||||
|
- './front:/app'
|
||||||
|
- 'front-node-modules:/app/node_modules'
|
||||||
|
db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
volumes:
|
||||||
|
- 'db-data:/var/lib/postgresql'
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: '${DB_PASS}'
|
||||||
|
POSTGRES_USER: '${DB_USER}'
|
||||||
|
POSTGRES_DB: '${DB_NAME}'
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
aliases:
|
||||||
|
- '${DB_HOST}'
|
||||||
|
server:
|
||||||
|
image: caddy:2.8.4-alpine
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
volumes:
|
||||||
|
- './config/caddy:/etc/caddy:ro'
|
||||||
|
- './volatile/caddy/log:/var/log/caddy'
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
volumes:
|
||||||
|
'back-node-modules':
|
||||||
|
'front-node-modules':
|
||||||
|
'db-data':
|
|
@ -0,0 +1,3 @@
|
||||||
|
Dockerfile
|
||||||
|
Dockerfile.dev
|
||||||
|
.dockerignore
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1,8 @@
|
||||||
|
FROM node:22-alpine3.20
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN yarn
|
||||||
|
|
||||||
|
CMD [ "yarn", "dev", "--host=front", "--port=80" ]
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Preact + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "front",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ky": "^1.7.2",
|
||||||
|
"preact": "^10.24.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.9.1",
|
||||||
|
"sass": "^1.80.6",
|
||||||
|
"sass-embedded": "^1.80.6",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^5.4.10"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import ky from "ky";
|
||||||
|
|
||||||
|
export type DataRowSearchOptions = {
|
||||||
|
page?: number,
|
||||||
|
page_sz?: number,
|
||||||
|
search?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataRow {
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public data: string
|
||||||
|
) { }
|
||||||
|
|
||||||
|
static reviveJSON(data: { id: number, data: string }): DataRow {
|
||||||
|
return new DataRow(data.id, data.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
static async find(options?: DataRowSearchOptions): Promise<DataRow[]> {
|
||||||
|
const req = await ky('/api/data/list', {
|
||||||
|
searchParams: options
|
||||||
|
});
|
||||||
|
|
||||||
|
const raw = await req.json<any[]>();
|
||||||
|
return raw.map(this.reviveJSON);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Index } from "./pages";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>4chan ripoff</h1>
|
||||||
|
<Index />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.posts {
|
||||||
|
display: block;
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { DataRow } from "../api/DataRow";
|
||||||
|
import style from './Posts.module.scss';
|
||||||
|
import { Post } from "./display/Post";
|
||||||
|
|
||||||
|
export type PostsProps = { rows: DataRow[] };
|
||||||
|
|
||||||
|
export function Posts({ rows }: PostsProps) {
|
||||||
|
return (
|
||||||
|
<div className={style.posts}>
|
||||||
|
{ rows.map(row => <Post row={row} />) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { TargetedEvent } from 'preact/compat';
|
||||||
|
|
||||||
|
export type PaginatorProps = {
|
||||||
|
page: number,
|
||||||
|
onChange: (page: number) => void,
|
||||||
|
className?: string
|
||||||
|
};
|
||||||
|
export function Paginator({page, onChange, className}: PaginatorProps) {
|
||||||
|
function middleware(e: TargetedEvent<HTMLInputElement, Event>) {
|
||||||
|
onChange(parseInt(e.currentTarget.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<label for='page'>Page: </label>
|
||||||
|
<input type='number' id='page' value={page} onChange={middleware}/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
.post {
|
||||||
|
border: 1px solid gray;
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
pre {
|
||||||
|
padding: 0; margin: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { DataRow } from "../../api/DataRow";
|
||||||
|
import style from './Post.module.scss';
|
||||||
|
|
||||||
|
export type PostProps = { row: DataRow };
|
||||||
|
export function Post({ row }: PostProps) {
|
||||||
|
return (
|
||||||
|
<div id={`post-${row.id}`} className={style.post}>
|
||||||
|
<pre>{row.data}</pre>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { render } from 'preact'
|
||||||
|
import { App } from './app.tsx'
|
||||||
|
|
||||||
|
render(<App />, document.getElementById('app')!)
|
|
@ -0,0 +1,6 @@
|
||||||
|
.index {
|
||||||
|
.paginator {
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { DataRow } from "../api/DataRow";
|
||||||
|
import style from './index.module.scss';
|
||||||
|
import { Posts } from "../components/Posts";
|
||||||
|
import { Paginator } from "../components/display/Paginator";
|
||||||
|
|
||||||
|
const cachedPages: {
|
||||||
|
[ key: number ]: DataRow[]
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
let firstRender = true;
|
||||||
|
|
||||||
|
export function Index() {
|
||||||
|
const [ data, setData ] = useState(null as null | DataRow[]);
|
||||||
|
const [ page, setPage ] = useState(NaN);
|
||||||
|
const [ pageBlocked, setPageBlocked ] = useState(false);
|
||||||
|
|
||||||
|
async function goToPage(newPage: number) {
|
||||||
|
if (newPage < 0) newPage = 0;
|
||||||
|
if (pageBlocked) return;
|
||||||
|
|
||||||
|
let newData = null as DataRow[] | null;
|
||||||
|
if (cachedPages[newPage]) {
|
||||||
|
newData = cachedPages[newPage];
|
||||||
|
} else {
|
||||||
|
setData(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPage(newPage);
|
||||||
|
setPageBlocked(true);
|
||||||
|
try {
|
||||||
|
newData = await DataRow.find({
|
||||||
|
page: newPage
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
return setPageBlocked(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newData === undefined) {
|
||||||
|
newData = [] as DataRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedPages[newPage] = newData;
|
||||||
|
setPageBlocked(false);
|
||||||
|
setData(newData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstRender) {
|
||||||
|
goToPage(0);
|
||||||
|
firstRender = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={style.index}>
|
||||||
|
<Paginator page={page} onChange={goToPage} className={style.paginator} />
|
||||||
|
{
|
||||||
|
data
|
||||||
|
? <Posts rows={data} />
|
||||||
|
: <pre>loading . . .</pre>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"paths": {
|
||||||
|
"react": ["./node_modules/preact/compat/"],
|
||||||
|
"react-dom": ["./node_modules/preact/compat/"]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import preact from '@preact/preset-vite'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [preact()],
|
||||||
|
})
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
Loading…
Reference in New Issue