initial commit
BIN
branding/logo-icon-black.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
18
branding/logo-icon-black.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path d="M276.893,509.396c7.018-0.559,13.986-1.427,20.894-2.545V5.085c-6.907-1.118-13.876-1.992-20.894-2.551V509.396z
|
||||
M368.646,484.04c4.912-2.426,9.765-5.041,14.496-7.777V35.672c-4.731-2.735-9.584-5.346-14.496-7.771V484.04z M322.773,501.449
|
||||
c5.96-1.617,11.871-3.494,17.661-5.47V16.086c-5.79-2.052-11.701-3.854-17.661-5.471V501.449z M460.41,407.38
|
||||
c2.805-3.793,5.47-7.646,8.086-11.561V116.181c-2.616-3.919-5.281-7.771-8.086-11.565V407.38z M506.232,210.68v90.582
|
||||
c2.675-14.736,4.103-29.841,4.103-45.324S508.907,225.41,506.232,210.68z M414.528,454.81c3.854-3.035,7.587-6.209,11.262-9.504
|
||||
V66.57c-3.675-3.295-7.408-6.405-11.262-9.514V454.81z M139.246,481.924c9.823,5.091,19.957,9.573,30.463,13.366V16.645
|
||||
c-10.506,3.794-20.699,8.271-30.463,13.367V481.924z M185.129,500.262c8.889,2.546,18.028,4.663,27.292,6.28V5.394
|
||||
c-9.264,1.617-18.403,3.729-27.292,6.28V500.262z M231.01,509.098c7.957,0.739,15.979,1.178,24.06,1.237V1.665
|
||||
c-8.146,0-16.162,0.435-24.06,1.244V509.098z M93.37,451.515c10.566,8.776,21.816,16.672,33.692,23.7V36.726
|
||||
c-11.876,7.028-23.126,14.925-33.692,23.69V451.515z M47.547,110.335v291.266c10.758,15.354,23.127,29.47,36.933,42.088V68.188
|
||||
C70.613,80.865,58.244,94.981,47.547,110.335z M41.768,392.954V118.916C16.4,158.453,1.665,205.453,1.665,255.938
|
||||
C1.665,306.423,16.4,353.423,41.768,392.954z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
branding/logo-icon-white.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
18
branding/logo-icon-white.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path fill="#FFFFFF" d="M276.818,508.887c7.004-0.565,13.955-1.436,20.831-2.545V5.605c-6.876-1.119-13.827-1.989-20.831-2.543
|
||||
V508.887z M368.382,483.572c4.912-2.418,9.747-5.026,14.458-7.758V36.123c-4.711-2.733-9.546-5.326-14.458-7.758V483.572z
|
||||
M322.601,500.876c5.956-1.61,11.851-3.474,17.619-5.467V16.514c-5.769-2.041-11.663-3.841-17.619-5.454V500.876z M459.969,407.072
|
||||
c2.784-3.778,5.455-7.632,8.061-11.536V116.465c-2.605-3.904-5.276-7.759-8.061-11.537V407.072z M505.738,210.772v90.393
|
||||
c2.67-14.698,4.105-29.786,4.105-45.228C509.844,240.495,508.358,225.407,505.738,210.772z M414.177,454.402
|
||||
c3.853-3.035,7.568-6.195,11.233-9.483V66.955c-3.665-3.288-7.381-6.385-11.233-9.483V454.402z M139.512,481.456
|
||||
c9.805,5.089,19.919,9.56,30.403,13.351V17.144c-10.484,3.777-20.662,8.249-30.403,13.337V481.456z M185.302,499.77
|
||||
c8.872,2.542,17.99,4.646,27.233,6.259V5.908c-9.243,1.612-18.361,3.729-27.233,6.273V499.77z M231.088,508.572
|
||||
c7.942,0.743,15.945,1.172,24.011,1.247V2.181c-8.128,0-16.132,0.441-24.011,1.247V508.572z M93.73,451.128
|
||||
c10.542,8.74,21.777,16.625,33.622,23.628V37.181c-11.845,7.016-23.08,14.888-33.622,23.642V451.128z M47.943,110.633v290.671
|
||||
c10.732,15.314,23.082,29.409,36.852,42.004V68.579C71.024,81.174,58.675,95.306,47.943,110.633z M42.169,392.677V119.197
|
||||
c-25.308,39.459-40.013,86.361-40.013,136.74S16.861,353.217,42.169,392.677z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
BIN
branding/logo-text-black.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
29
branding/logo-text-black.svg
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="505.816px" height="111.412px" viewBox="0 0 505.816 111.412" enable-background="new 0 0 505.816 111.412"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path d="M18.612,106.414c-5.911-3.336-10.5-8.051-13.738-14.119C1.603,86.233,0,79.19,0,71.176V40.24
|
||||
c0-8.017,1.636-15.059,4.874-21.126c3.238-6.068,7.828-10.783,13.738-14.115C24.521,1.668,31.343,0,39.108,0
|
||||
c7.765,0,14.585,1.668,20.497,4.999c5.912,3.332,10.468,8.047,13.738,14.115c3.238,6.067,4.872,13.141,4.872,21.126v30.936
|
||||
c0,8.015-1.634,15.058-4.872,21.119c-3.271,6.068-7.827,10.783-13.738,14.119c-5.911,3.333-12.731,4.998-20.497,4.998
|
||||
C31.343,111.412,24.521,109.712,18.612,106.414z M49.953,90.883c3.112-1.786,5.532-4.367,7.231-7.704
|
||||
c1.696-3.332,2.546-7.163,2.546-11.534v-31.97c0-4.339-0.848-8.205-2.546-11.54c-1.699-3.331-4.12-5.908-7.231-7.7
|
||||
c-3.143-1.792-6.76-2.704-10.845-2.704c-4.087,0-7.733,0.912-10.846,2.704c-3.143,1.792-5.534,4.369-7.23,7.7
|
||||
c-1.698,3.335-2.545,7.168-2.545,11.54v31.97c0,4.371,0.847,8.202,2.545,11.534c1.696,3.337,4.118,5.882,7.23,7.704
|
||||
c3.112,1.797,6.729,2.705,10.846,2.705C43.226,93.588,46.841,92.711,49.953,90.883z"/>
|
||||
<path d="M175.069,1.1h20.716v109.146h-16.813v-83.24l0.751,4.811l-23.856,64.792h-14.397l-23.863-63.283l0.752-6.319v83.24h-16.818
|
||||
V1.1h20.718l26.409,72.812L175.069,1.1z"/>
|
||||
<path d="M237.724,106.632c-5.845-3.17-10.307-7.794-13.358-13.892c-3.047-6.068-4.592-13.394-4.592-21.944V1.133h17.95v70.199
|
||||
c0,7.103,1.827,12.604,5.504,16.502c3.674,3.896,8.863,5.845,15.588,5.845c6.764,0,11.984-1.948,15.688-5.845
|
||||
c3.711-3.897,5.563-9.399,5.563-16.502V1.133h17.953v69.663c0,8.551-1.535,15.876-4.619,21.944
|
||||
c-3.086,6.066-7.549,10.722-13.393,13.892c-5.846,3.18-12.924,4.78-21.156,4.78C250.618,111.382,243.601,109.812,237.724,106.632z"
|
||||
/>
|
||||
<path d="M322.037,1.1h17.951v109.146h-17.951V1.1z M334.268,65.011l45.928-63.878h22.384l-68.155,88.745L334.268,65.011z
|
||||
M352.437,53.507l14.872-11.191l39.857,67.93h-21.535L352.437,53.507z"/>
|
||||
<path d="M420.688,1.1h17.949v109.146h-17.949V1.1z M432.916,65.011l45.928-63.878h22.384l-68.154,88.745L432.916,65.011z
|
||||
M451.085,53.507l14.872-11.191l39.859,67.93h-21.535L451.085,53.507z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
branding/logo-text-white.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
29
branding/logo-text-white.svg
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="505.541px" height="111.352px" viewBox="0 0 505.541 111.352" enable-background="new 0 0 505.541 111.352"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M18.602,106.356c-5.907-3.334-10.494-8.047-13.73-14.111C1.602,86.187,0,79.147,0,71.137V40.218
|
||||
c0-8.012,1.635-15.05,4.871-21.114c3.236-6.065,7.823-10.777,13.73-14.107C24.508,1.667,31.326,0,39.087,0
|
||||
s14.577,1.667,20.485,4.997c5.908,3.33,10.462,8.042,13.73,14.107c3.236,6.064,4.869,13.133,4.869,21.114v30.918
|
||||
c0,8.011-1.633,15.05-4.869,21.108c-3.269,6.064-7.822,10.777-13.73,14.111c-5.908,3.331-12.725,4.995-20.485,4.995
|
||||
S24.508,109.652,18.602,106.356z M49.926,90.833c3.11-1.785,5.529-4.364,7.228-7.699c1.695-3.33,2.545-7.159,2.545-11.528V39.653
|
||||
c0-4.336-0.848-8.201-2.545-11.534c-1.698-3.33-4.117-5.905-7.228-7.696c-3.141-1.791-6.756-2.703-10.839-2.703
|
||||
c-4.085,0-7.729,0.912-10.84,2.703c-3.142,1.791-5.531,4.367-7.227,7.696c-1.697,3.333-2.544,7.164-2.544,11.534v31.952
|
||||
c0,4.369,0.847,8.198,2.544,11.528c1.695,3.335,4.115,5.878,7.227,7.699c3.11,1.796,6.725,2.704,10.84,2.704
|
||||
S46.815,92.66,49.926,90.833z"/>
|
||||
<path fill="#FFFFFF" d="M174.974,1.099h20.705v109.086h-16.805V26.99l0.751,4.808l-23.844,64.756h-14.39l-23.85-63.249l0.752-6.316
|
||||
v83.195h-16.809V1.099h20.707l26.395,72.772L174.974,1.099z"/>
|
||||
<path fill="#FFFFFF" d="M237.595,106.574c-5.842-3.169-10.302-7.79-13.351-13.885c-3.046-6.064-4.59-13.386-4.59-21.932V1.132
|
||||
h17.94v70.161c0,7.099,1.826,12.598,5.501,16.493c3.672,3.895,8.858,5.842,15.58,5.842c6.76,0,11.978-1.947,15.679-5.842
|
||||
c3.709-3.896,5.56-9.395,5.56-16.493V1.132h17.943v69.625c0,8.546-1.534,15.867-4.617,21.932
|
||||
c-3.084,6.063-7.544,10.716-13.385,13.885c-5.843,3.178-12.917,4.777-21.145,4.777
|
||||
C250.481,111.321,243.468,109.752,237.595,106.574z"/>
|
||||
<path fill="#FFFFFF" d="M321.861,1.099h17.941v109.086h-17.941V1.099z M334.086,64.976l45.902-63.843h22.371l-68.117,88.697
|
||||
L334.086,64.976z M352.244,53.478l14.864-11.185l39.836,67.893h-21.523L352.244,53.478z"/>
|
||||
<path fill="#FFFFFF" d="M420.458,1.099h17.939v109.086h-17.939V1.099z M432.681,64.976l45.902-63.843h22.371l-68.117,88.697
|
||||
L432.681,64.976z M450.839,53.478l14.864-11.185l39.838,67.893h-21.523L450.839,53.478z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
1
epaper
Submodule
24
epaper-client/.gitignore
vendored
Normal file
@ -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?
|
||||
54
epaper-client/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
extends: [
|
||||
// Remove ...tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config({
|
||||
plugins: {
|
||||
// Add the react-x and react-dom plugins
|
||||
'react-x': reactX,
|
||||
'react-dom': reactDom,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended typescript rules
|
||||
...reactX.configs['recommended-typescript'].rules,
|
||||
...reactDom.configs.recommended.rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
BIN
epaper-client/bun.lockb
Normal file
21
epaper-client/components.json
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
28
epaper-client/eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
epaper-client/index.html
Normal file
@ -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 + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2538
epaper-client/package-lock.json
generated
Normal file
43
epaper-client/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "epaper-client",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^19.0.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-grid-layout": "^1.5.1",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@types/react-grid-layout": "^1.3.5",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"tw-animate-css": "^1.2.8",
|
||||
"typescript": "~5.7.2",
|
||||
"typescript-eslint": "^8.26.1",
|
||||
"vite": "^6.3.1"
|
||||
}
|
||||
}
|
||||
BIN
epaper-client/public/((2_r1_c2)_g1.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
epaper-client/public/(1_r4_c1).jpg
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
epaper-client/public/(1_r4_c2).jpg
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
epaper-client/public/(2_r1_c2)_g1.jpg
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
epaper-client/public/(2_r1_c4_g5.jpg
Normal file
|
After Width: | Height: | Size: 203 KiB |
BIN
epaper-client/public/(2_r3_c3)_g5.jpg
Normal file
|
After Width: | Height: | Size: 119 KiB |
BIN
epaper-client/public/1_r2_c1.jpg
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
epaper-client/public/1_r2_c1_g6.jpg
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
epaper-client/public/1_r2_c6.jpg
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
epaper-client/public/1_r3_c1.jpg
Normal file
|
After Width: | Height: | Size: 68 KiB |
BIN
epaper-client/public/1_r3_c2.jpg
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
epaper-client/public/1_r3_c3.jpg
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
epaper-client/public/1_r4_c2.jpg
Normal file
|
After Width: | Height: | Size: 126 KiB |
BIN
epaper-client/public/2_r1_c1_g8).jpg
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
epaper-client/public/2_r1_c1_g8.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
epaper-client/public/2_r1_c2_g3).jpg
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
epaper-client/public/2_r1_c2_g3.jpg
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
epaper-client/public/2_r1_c4_g5.jpg
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
epaper-client/public/2_r1_c8.jpg
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
epaper-client/public/2_r2_c2_g1).jpg
Normal file
|
After Width: | Height: | Size: 258 KiB |
BIN
epaper-client/public/2_r2_c2_g1.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |
BIN
epaper-client/public/2_r2_c2_g3.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
epaper-client/public/2_r2_c3_g5.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
epaper-client/public/2_r3_c3_g3.jpg
Normal file
|
After Width: | Height: | Size: 114 KiB |
1
epaper-client/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
25
epaper-client/src/App.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import Header from "./components/Header";
|
||||
import DateHeader from "./components/DateHeader";
|
||||
import Sidebar from "./components/Sidebar";
|
||||
import NewspaperViewer from "./components/NewspaperViewer";
|
||||
import { DateProvider } from "./context/DateContext";
|
||||
import { PageProvider } from "./context/PageContext";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<PageProvider>
|
||||
<DateProvider>
|
||||
<main className="pt-4">
|
||||
<Header />
|
||||
<DateHeader />
|
||||
<section className="px-96 flex pt-8 gap-7">
|
||||
<NewspaperViewer />
|
||||
<Sidebar />
|
||||
</section>
|
||||
</main>
|
||||
</DateProvider>
|
||||
</PageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
1
epaper-client/src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
21
epaper-client/src/components/DateHeader.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import { weekdays } from "../../utils/date-info";
|
||||
import { months } from "../../utils/date-info";
|
||||
|
||||
const DateHeader = () => {
|
||||
const currentDate = new Date();
|
||||
|
||||
return (
|
||||
<div className="bg-slate-500 px-96 py-2 flex justify-between">
|
||||
<section className="flex gap-10">
|
||||
<button className="text-white font-bold">Home</button>
|
||||
</section>
|
||||
<h1 className="text-white">
|
||||
{weekdays[currentDate.getDay() - 1]} ● {currentDate.getDate()}{" "}
|
||||
{months[currentDate.getMonth()]} {currentDate.getFullYear()}
|
||||
</h1>
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateHeader;
|
||||
9
epaper-client/src/components/Header.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
const Header = () => {
|
||||
return (
|
||||
<div className="flex justify-between items-center px-96">
|
||||
<img src="/logo.png" alt="" className="w-60" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
280
epaper-client/src/components/NewspaperViewer.tsx
Normal file
@ -0,0 +1,280 @@
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { useDateContext } from "@/context/DateContext";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
autoPlaceArticles,
|
||||
createArticlesFromFilenames,
|
||||
isSameDay,
|
||||
} from "../../utils/helpers";
|
||||
import { usePageContext } from "@/context/PageContext";
|
||||
|
||||
// Type Definitions
|
||||
interface ArticleImage {
|
||||
page: number;
|
||||
date: string;
|
||||
articles: string[];
|
||||
}
|
||||
|
||||
interface Origin {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
const NewspaperViewer = () => {
|
||||
const { currentPage, setCurrentPage } = usePageContext();
|
||||
const [selectedArticle, setSelectedArticle] = useState<string | null>(null);
|
||||
const [images, setImages] = useState<ArticleImage[]>([]);
|
||||
const [colCount, setColCount] = useState(8);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { date } = useDateContext();
|
||||
const [origin, setOrigin] = useState<Origin>({ x: 0, y: 0 });
|
||||
|
||||
// Update column count based on page
|
||||
useEffect(() => {
|
||||
setColCount(currentPage === 1 ? 6 : 8);
|
||||
}, [currentPage]);
|
||||
|
||||
// Simulated async fetch (replace with actual fetch if needed)
|
||||
useEffect(() => {
|
||||
const fetchImages = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
// Simulate delay
|
||||
await new Promise((res) => setTimeout(res, 500));
|
||||
|
||||
setImages([
|
||||
{
|
||||
page: 1,
|
||||
date: "Mon May 13 2025 22:00:46 GMT+0600 (Bangladesh Standard Time)",
|
||||
articles: [
|
||||
"1_r2_c6.jpg",
|
||||
"1_r3_c3.jpg",
|
||||
"1_r3_c1.jpg",
|
||||
"1_r3_c2.jpg",
|
||||
"1_r4_c2.jpg",
|
||||
"(1_r4_c1).jpg",
|
||||
"(1_r4_c2).jpg",
|
||||
"1_r2_c1.jpg",
|
||||
"1_r2_c1_g6.jpg",
|
||||
],
|
||||
},
|
||||
{
|
||||
page: 2,
|
||||
date: "Mon May 13 2025 22:00:00 GMT+0600 (Bangladesh Standard Time)",
|
||||
articles: [
|
||||
"2_r1_c8.jpg",
|
||||
"2_r2_c2_g1).jpg",
|
||||
"2_r1_c2_g3.jpg",
|
||||
"2_r1_c4_g5.jpg",
|
||||
"(2_r1_c4_g5.jpg",
|
||||
"2_r1_c1_g8).jpg",
|
||||
"2_r1_c1_g8.jpg",
|
||||
"2_r1_c2_g3).jpg",
|
||||
"2_r2_c2_g3.jpg",
|
||||
"2_r2_c3_g5.jpg",
|
||||
"2_r3_c3_g3.jpg",
|
||||
"(2_r3_c3)_g5.jpg",
|
||||
"(2_r1_c2)_g1.jpg",
|
||||
"((2_r1_c2)_g1.jpg",
|
||||
"2_r2_c2_g1.jpg",
|
||||
],
|
||||
},
|
||||
]);
|
||||
} catch (err) {
|
||||
setError("Failed to load newspaper data.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchImages();
|
||||
}, []);
|
||||
|
||||
const zoomIn = (articleUrl: string, e: React.MouseEvent) => {
|
||||
const { clientX, clientY } = e;
|
||||
setOrigin({ x: clientX, y: clientY });
|
||||
setSelectedArticle(articleUrl);
|
||||
};
|
||||
|
||||
const zoomOut = () => {
|
||||
setSelectedArticle(null);
|
||||
};
|
||||
|
||||
const dateFilteredContent = useMemo(
|
||||
() => images.filter((item) => isSameDay(item.date, date)),
|
||||
[images, date]
|
||||
);
|
||||
|
||||
const hasContentForDate = dateFilteredContent.length > 0;
|
||||
|
||||
const dateArticles = useMemo(
|
||||
() =>
|
||||
hasContentForDate
|
||||
? dateFilteredContent.flatMap((pageGroup) =>
|
||||
createArticlesFromFilenames(pageGroup.articles, pageGroup.page)
|
||||
)
|
||||
: [],
|
||||
[dateFilteredContent, hasContentForDate]
|
||||
);
|
||||
|
||||
const currentPageArticles = dateArticles.filter(
|
||||
(a) => a.page === currentPage
|
||||
);
|
||||
const placedArticles = useMemo(
|
||||
() => autoPlaceArticles(currentPageArticles, colCount),
|
||||
[currentPageArticles, colCount]
|
||||
);
|
||||
|
||||
const totalPages = hasContentForDate
|
||||
? Math.max(...dateFilteredContent.map((item) => item.page))
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="w-3/4 mx-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center h-64">Loading...</div>
|
||||
) : error ? (
|
||||
<div className="text-red-600 text-center h-64 flex items-center justify-center">
|
||||
{error}
|
||||
</div>
|
||||
) : hasContentForDate ? (
|
||||
<>
|
||||
{/* Pagination */}
|
||||
<section className="my-4">
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCurrentPage((prev: number) => Math.max(1, prev - 1));
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||
(n) => (
|
||||
<PaginationItem key={n}>
|
||||
<PaginationLink
|
||||
href="#"
|
||||
isActive={n === currentPage}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCurrentPage(n);
|
||||
}}
|
||||
>
|
||||
{n}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</section>
|
||||
|
||||
{/* Articles */}
|
||||
<section
|
||||
className="gap-1"
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: `repeat(${colCount}, 1fr)`,
|
||||
}}
|
||||
>
|
||||
{placedArticles.map((article) => (
|
||||
<div
|
||||
key={article.id}
|
||||
style={{
|
||||
gridColumn: `${article.gridColumnStart} / span ${article.colSpan}`,
|
||||
gridRow: `${article.gridRowStart} / span ${article.rowSpan}`,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
className="hover:opacity-60 hover:cursor-pointer transition-opacity"
|
||||
>
|
||||
<img
|
||||
onClick={(e) => zoomIn(`/${article.image}`, e)}
|
||||
src={`/${article.image}`}
|
||||
alt={`Article ${article.id}`}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
display: "block",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-64 text-center">
|
||||
<h2 className="text-2xl font-semibold mb-2">
|
||||
No newspaper available
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
There is no newspaper edition for{" "}
|
||||
{date?.toLocaleDateString("en-US", {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Zoomed View */}
|
||||
{selectedArticle && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/70 flex justify-center items-center z-50"
|
||||
onClick={zoomOut}
|
||||
>
|
||||
<div
|
||||
className="relative bg-white rounded-lg overflow-hidden shadow-lg"
|
||||
style={{
|
||||
animation: "zoomIn 0.3s ease-out",
|
||||
transformOrigin: `${origin.x}px ${origin.y}px`,
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<img
|
||||
src={selectedArticle}
|
||||
alt="Zoomed Article"
|
||||
className="rounded max-h-[90vh] max-w-[90vw]"
|
||||
/>
|
||||
<button
|
||||
onClick={zoomOut}
|
||||
className="absolute top-2 right-2 text-black text-2xl"
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewspaperViewer;
|
||||
54
epaper-client/src/components/Sidebar.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
"use client";
|
||||
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import { useDateContext } from "@/context/DateContext";
|
||||
import { usePageContext } from "@/context/PageContext";
|
||||
import classNames from "classnames"; // Optional: use if you're using clsx/classnames for cleaner conditional styling
|
||||
|
||||
const Sidebar = () => {
|
||||
const { date, setDate } = useDateContext();
|
||||
const { currentPage, setCurrentPage } = usePageContext();
|
||||
|
||||
const pages = [
|
||||
`${date?.toLocaleDateString()}`,
|
||||
...Array.from({ length: 10 }, (_, i) => `Page ${i + 1}`),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-1/4 flex flex-col gap-6 p-4">
|
||||
<h1 className="text-2xl font-semibold">Today's Paper</h1>
|
||||
|
||||
{/* Page Buttons */}
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
{pages.map((page, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
className={classNames(
|
||||
"py-2 px-4 text-left text-lg tracking-tight rounded transition-colors duration-200 hover:cursor-pointer",
|
||||
{
|
||||
"bg-slate-800 text-white": currentPage === idx,
|
||||
"bg-slate-200 hover:bg-slate-300 text-black":
|
||||
currentPage !== idx,
|
||||
"bg-slate-700 text-white": idx === 0, // disabled style
|
||||
}
|
||||
)}
|
||||
disabled={idx === 0}
|
||||
onClick={() => setCurrentPage(idx)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Calendar Date Picker */}
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={date}
|
||||
onSelect={setDate}
|
||||
className="border rounded-md p-2 shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
59
epaper-client/src/components/ui/button.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Button, buttonVariants }
|
||||
73
epaper-client/src/components/ui/calendar.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import * as React from "react";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { DayPicker } from "react-day-picker";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1 hover:cursor-pointer",
|
||||
nav_button_next: "absolute right-1 hover:cursor-pointer",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100 hover:cursor-pointer"
|
||||
),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar };
|
||||
127
epaper-client/src/components/ui/pagination.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("mx-auto flex w-full justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||
React.ComponentProps<"a">
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
size = "icon",
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? "outline" : "ghost",
|
||||
size,
|
||||
}),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
<span className="hidden sm:block">Previous</span>
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
size="default"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<span className="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationLink,
|
||||
PaginationItem,
|
||||
PaginationPrevious,
|
||||
PaginationNext,
|
||||
PaginationEllipsis,
|
||||
}
|
||||
31
epaper-client/src/context/DateContext.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
// 1. Create the context
|
||||
type DateContextType = {
|
||||
date: Date | undefined;
|
||||
setDate: (date: Date | undefined) => void;
|
||||
};
|
||||
|
||||
const DateContext = React.createContext<DateContextType | undefined>(undefined);
|
||||
|
||||
// 2. Create a custom hook to use the context
|
||||
export function useDateContext() {
|
||||
const context = React.useContext(DateContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useDateContext must be used within a DateProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
// 3. Create the provider component
|
||||
export function DateProvider({ children }: { children: React.ReactNode }) {
|
||||
const [date, setDate] = React.useState<Date | undefined>(new Date());
|
||||
|
||||
const value = React.useMemo(() => {
|
||||
return { date, setDate };
|
||||
}, [date]);
|
||||
|
||||
return <DateContext.Provider value={value}>{children}</DateContext.Provider>;
|
||||
}
|
||||
28
epaper-client/src/context/PageContext.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
type PageContextType = {
|
||||
currentPage: Number | undefined;
|
||||
setCurrentPage: (currentPage: Number | undefined) => void;
|
||||
};
|
||||
|
||||
const PageContext = React.createContext<PageContextType | undefined>(undefined);
|
||||
|
||||
export function usePageContext() {
|
||||
const context = React.useContext(PageContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("usePageContext must be used within a PageProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export function PageProvider({ children }: { children: React.ReactNode }) {
|
||||
const [currentPage, setCurrentPage] = React.useState<Number | undefined>(1);
|
||||
|
||||
const value = React.useMemo(() => {
|
||||
return { currentPage, setCurrentPage };
|
||||
}, [currentPage]);
|
||||
|
||||
return <PageContext.Provider value={value}>{children}</PageContext.Provider>;
|
||||
}
|
||||
131
epaper-client/src/index.css
Normal file
@ -0,0 +1,131 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoomIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.3);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
6
epaper-client/src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
10
epaper-client/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
1
epaper-client/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
32
epaper-client/tsconfig.app.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
13
epaper-client/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
24
epaper-client/tsconfig.node.json
Normal file
@ -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"]
|
||||
}
|
||||
23
epaper-client/utils/date-info.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const months = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
export const weekdays = [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
];
|
||||
108
epaper-client/utils/helpers.ts
Normal file
@ -0,0 +1,108 @@
|
||||
export function autoPlaceArticles(articles, columnCount) {
|
||||
const columnHeights = Array(columnCount).fill(0); // Track max height per column
|
||||
const placements = [];
|
||||
|
||||
for (let article of articles) {
|
||||
const { colSpan, rowSpan, gridColumnStart } = article;
|
||||
|
||||
let colStart, rowStart;
|
||||
|
||||
if (gridColumnStart) {
|
||||
// Manual column override
|
||||
colStart = gridColumnStart;
|
||||
// Get current max height in that column group
|
||||
const currentHeight = Math.max(
|
||||
...columnHeights.slice(colStart - 1, colStart - 1 + colSpan)
|
||||
);
|
||||
rowStart = currentHeight + 1;
|
||||
} else {
|
||||
// Auto-placement: find best column group with lowest stack height
|
||||
let bestStart = 0;
|
||||
let minHeight = Infinity;
|
||||
|
||||
for (let start = 0; start <= columnCount - colSpan; start++) {
|
||||
const slice = columnHeights.slice(start, start + colSpan);
|
||||
const height = Math.max(...slice);
|
||||
if (height < minHeight) {
|
||||
minHeight = height;
|
||||
bestStart = start;
|
||||
}
|
||||
}
|
||||
|
||||
colStart = bestStart + 1;
|
||||
rowStart = minHeight + 1;
|
||||
}
|
||||
|
||||
// Place the article
|
||||
placements.push({
|
||||
...article,
|
||||
gridColumnStart: colStart,
|
||||
gridRowStart: rowStart,
|
||||
});
|
||||
|
||||
// Update column heights
|
||||
for (let i = colStart - 1; i < colStart - 1 + colSpan; i++) {
|
||||
columnHeights[i] = rowStart - 1 + rowSpan;
|
||||
}
|
||||
}
|
||||
|
||||
return placements;
|
||||
}
|
||||
|
||||
export function parseImageFilename(filename) {
|
||||
// Updated regex to capture optional gridColumnStart (_gZ) and handle parentheses/brackets
|
||||
const regex =
|
||||
/(?<page>\d+)_r(?<rowSpan>\d+)_c(?<colSpan>\d+)(?:_g(?<gridColumnStart>\d+))?.*\.\w+$/;
|
||||
const match = filename.match(regex);
|
||||
|
||||
if (!match || !match.groups) {
|
||||
throw new Error(`Invalid filename format: ${filename}`);
|
||||
}
|
||||
|
||||
const { page, rowSpan, colSpan, gridColumnStart } = match.groups;
|
||||
|
||||
return {
|
||||
page: parseInt(page, 10),
|
||||
rowSpan: parseInt(rowSpan, 10),
|
||||
colSpan: parseInt(colSpan, 10),
|
||||
// Only include gridColumnStart if it exists in the filename
|
||||
...(gridColumnStart && { gridColumnStart: parseInt(gridColumnStart, 10) }),
|
||||
};
|
||||
}
|
||||
|
||||
export function createArticlesFromFilenames(filenames, pageNumber) {
|
||||
return filenames
|
||||
.map((filename, index) => {
|
||||
try {
|
||||
const properties = parseImageFilename(filename);
|
||||
return {
|
||||
id: index + 1,
|
||||
page: pageNumber, // inject page from outer structure
|
||||
colSpan: properties.colSpan,
|
||||
rowSpan: properties.rowSpan,
|
||||
image: filename,
|
||||
...(properties.gridColumnStart && {
|
||||
gridColumnStart: properties.gridColumnStart,
|
||||
}),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error processing filename ${filename}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((article) => article !== null);
|
||||
}
|
||||
|
||||
export function isSameDay(date1, date2) {
|
||||
if (!date1 || !date2) return false;
|
||||
|
||||
// Convert string dates to Date objects if needed
|
||||
const d1 = typeof date1 === "string" ? new Date(date1) : date1;
|
||||
const d2 = typeof date2 === "string" ? new Date(date2) : date2;
|
||||
|
||||
return (
|
||||
d1.getFullYear() === d2.getFullYear() &&
|
||||
d1.getMonth() === d2.getMonth() &&
|
||||
d1.getDate() === d2.getDate()
|
||||
);
|
||||
}
|
||||
14
epaper-client/vite.config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import path from "path";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
24
epaper-frontend/.gitignore
vendored
Normal file
@ -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?
|
||||
12
epaper-frontend/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
BIN
epaper-frontend/bun.lockb
Normal file
22
epaper-frontend/components/DateHeader.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from "react";
|
||||
import { weekdays } from "../utils/date-info";
|
||||
import { months } from "../utils/date-info";
|
||||
|
||||
const DateHeader = () => {
|
||||
const currentDate = new Date();
|
||||
|
||||
return (
|
||||
<div className="bg-slate-500 px-96 py-2 flex justify-between">
|
||||
<section className="flex gap-10">
|
||||
<button className="text-white font-bold">Home</button>
|
||||
</section>
|
||||
<h1 className="text-white">
|
||||
{weekdays[currentDate.getDay() - 1]} ● {currentDate.getDate()}{" "}
|
||||
{months[currentDate.getMonth() - 1]} {currentDate.getFullYear()}
|
||||
</h1>
|
||||
<div></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateHeader;
|
||||
11
epaper-frontend/components/Header.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<div className="flex justify-between items-center px-96">
|
||||
<img src="/logo.png" alt="" className="w-60" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
12
epaper-frontend/components/NewspaperViewer.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from "react";
|
||||
|
||||
const NewspaperViewer = () => {
|
||||
return (
|
||||
<div className="w-3/4">
|
||||
<section>Pagination</section>
|
||||
<section>Newspaper</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewspaperViewer;
|
||||
11
epaper-frontend/components/Sidebar.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
const Sidebar = () => {
|
||||
return (
|
||||
<div className="w-1/4">
|
||||
<h1 className="text-2xl">Today's Paper</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
33
epaper-frontend/eslint.config.js
Normal file
@ -0,0 +1,33 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...js.configs.recommended.rules,
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
13
epaper-frontend/index.html
Normal file
@ -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 + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
2054
epaper-frontend/package-lock.json
generated
Normal file
33
epaper-frontend/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "epaper-frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-grid-layout": "^1.5.1",
|
||||
"tailwindcss": "^4.1.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.19",
|
||||
"globals": "^16.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.1"
|
||||
}
|
||||
}
|
||||
BIN
epaper-frontend/public/logo.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
1
epaper-frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
21
epaper-frontend/src/App.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
// App.jsx - Main application component
|
||||
import React from "react";
|
||||
import Header from "../components/Header";
|
||||
import DateHeader from "../components/DateHeader";
|
||||
import Sidebar from "../components/Sidebar";
|
||||
import NewspaperViewer from "../components/NewspaperViewer";
|
||||
|
||||
const App = () => {
|
||||
return (
|
||||
<main className="pt-4">
|
||||
<Header />
|
||||
<DateHeader />
|
||||
<section className="px-96 flex pt-8 gap-7">
|
||||
<Sidebar />
|
||||
<NewspaperViewer />
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
10
epaper-frontend/src/index.css
Normal file
@ -0,0 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
10
epaper-frontend/src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.js";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
||||
20
epaper-frontend/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
23
epaper-frontend/utils/date-info.js
Normal file
@ -0,0 +1,23 @@
|
||||
export const months = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
export const weekdays = [
|
||||
"Monday",
|
||||
"Tuesday",
|
||||
"Wednesday",
|
||||
"Thursday",
|
||||
"Friday",
|
||||
"Saturday",
|
||||
"Sunday",
|
||||
];
|
||||
14
epaper-frontend/vite.config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import path from "path";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
});
|
||||
1
examjam
Submodule
24
examjam-admin/.gitignore
vendored
Normal file
@ -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?
|
||||
50
examjam-admin/README.md
Normal file
@ -0,0 +1,50 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
|
||||
|
||||
- Configure the top-level `parserOptions` property like this:
|
||||
|
||||
```js
|
||||
export default tseslint.config({
|
||||
languageOptions: {
|
||||
// other options...
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
|
||||
- Optionally add `...tseslint.configs.stylisticTypeChecked`
|
||||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import react from 'eslint-plugin-react'
|
||||
|
||||
export default tseslint.config({
|
||||
// Set the react version
|
||||
settings: { react: { version: '18.3' } },
|
||||
plugins: {
|
||||
// Add the react plugin
|
||||
react,
|
||||
},
|
||||
rules: {
|
||||
// other rules...
|
||||
// Enable its recommended rules
|
||||
...react.configs.recommended.rules,
|
||||
...react.configs['jsx-runtime'].rules,
|
||||
},
|
||||
})
|
||||
```
|
||||
BIN
examjam-admin/bun.lockb
Normal file
233
examjam-admin/components/ExamBuilder.tsx
Normal file
@ -0,0 +1,233 @@
|
||||
import React, { useState } from "react";
|
||||
|
||||
const ExamBuilder = () => {
|
||||
const [paper, setPaper] = useState({
|
||||
id: "",
|
||||
title: "",
|
||||
data: {
|
||||
metadata: {
|
||||
type: "single",
|
||||
marking: "",
|
||||
duration: 0,
|
||||
quantity: 0,
|
||||
},
|
||||
questions: [
|
||||
{
|
||||
id: 1,
|
||||
type: "single",
|
||||
options: {
|
||||
a: "",
|
||||
b: "",
|
||||
c: "",
|
||||
d: "",
|
||||
},
|
||||
question: "",
|
||||
solution: "",
|
||||
correctAnswer: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setPaper((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleMetadataChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setPaper((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
metadata: {
|
||||
...prev.data.metadata,
|
||||
[name]: value,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleQuestionChange = (index, field, value) => {
|
||||
const updatedQuestions = paper.data.questions.map((q, i) =>
|
||||
i === index ? { ...q, [field]: value } : q
|
||||
);
|
||||
setPaper((prev) => ({
|
||||
...prev,
|
||||
data: { ...prev.data, questions: updatedQuestions },
|
||||
}));
|
||||
};
|
||||
|
||||
const handleOptionChange = (index, optionKey, value) => {
|
||||
const updatedQuestions = paper.data.questions.map((q, i) =>
|
||||
i === index ? { ...q, options: { ...q.options, [optionKey]: value } } : q
|
||||
);
|
||||
setPaper((prev) => ({
|
||||
...prev,
|
||||
data: { ...prev.data, questions: updatedQuestions },
|
||||
}));
|
||||
};
|
||||
|
||||
const addQuestion = () => {
|
||||
setPaper((prev) => ({
|
||||
...prev,
|
||||
data: {
|
||||
...prev.data,
|
||||
questions: [
|
||||
...prev.data.questions,
|
||||
{
|
||||
id: prev.data.questions.length + 1,
|
||||
type: "single",
|
||||
options: { a: "", b: "", c: "", d: "" },
|
||||
question: "",
|
||||
solution: "",
|
||||
correctAnswer: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
console.log("Exam Paper Submitted: ", paper);
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="flex w-3/4 gap-10 h-fit">
|
||||
<section className="border-1 border-blue-100 rounded-2xl mt-5 py-10 px-10 max-h-[70vh] w-1/2">
|
||||
<h1 className="text-white text-3xl font-bold text-center mb-6">
|
||||
Exam Builder
|
||||
</h1>
|
||||
<form className="flex flex-col gap-4" onSubmit={handleSubmit}>
|
||||
<h2 className="text-white text-2xl">General Information</h2>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
value={paper.title}
|
||||
onChange={handleChange}
|
||||
placeholder="Exam Title"
|
||||
className="bg-white w-full rounded-full py-3 px-6"
|
||||
/>
|
||||
|
||||
<h2 className="text-white text-2xl">Metadata</h2>
|
||||
<select
|
||||
name="type"
|
||||
value={paper.data.metadata.type}
|
||||
onChange={handleMetadataChange}
|
||||
className="bg-white p-4 rounded-full"
|
||||
>
|
||||
<option value="single">Multiple Choice Questions (Single)</option>
|
||||
<option value="multiple">
|
||||
Multiple Choice Questions (Multiple)
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
name="marking"
|
||||
value={paper.data.metadata.marking}
|
||||
onChange={handleMetadataChange}
|
||||
placeholder="Marking"
|
||||
className="bg-white w-full rounded-full py-3 px-6"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
name="duration"
|
||||
value={paper.data.metadata.duration}
|
||||
onChange={handleMetadataChange}
|
||||
placeholder="Duration"
|
||||
className="bg-white w-full rounded-full py-3 px-6"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
name="quantity"
|
||||
value={paper.data.metadata.quantity}
|
||||
onChange={handleMetadataChange}
|
||||
placeholder="How many questions?"
|
||||
className="bg-white w-full rounded-full py-3 px-6"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-blue-500 text-white p-3 rounded-full mt-4"
|
||||
>
|
||||
Submit Exam
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
<section className="border-1 border-blue-100 rounded-2xl mt-5 py-10 px-10 w-1/2">
|
||||
<h1 className="text-white text-3xl font-bold text-center mb-6">
|
||||
Questions
|
||||
</h1>
|
||||
{paper.data.questions.map((question, index) => (
|
||||
<div key={index} className="rounded-lg">
|
||||
<h2 className="text-white text-xl mb-4">Question {index + 1}</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={question.question}
|
||||
onChange={(e) =>
|
||||
handleQuestionChange(index, "question", e.target.value)
|
||||
}
|
||||
placeholder="Question"
|
||||
className="bg-white w-full rounded-full py-3 px-6 mb-2"
|
||||
/>
|
||||
<div className="">
|
||||
{Object.keys(question.options).map((key) => (
|
||||
<div key={key} className="flex items-center gap-2 mb-2">
|
||||
<h3 className="text-white uppercase py-3 px-5 rounded-full bg-blue-600">
|
||||
{key}
|
||||
</h3>
|
||||
<input
|
||||
type="text"
|
||||
value={question.options[key]}
|
||||
onChange={(e) =>
|
||||
handleOptionChange(index, key, e.target.value)
|
||||
}
|
||||
placeholder={`Option ${key.toUpperCase()}`}
|
||||
className="bg-white w-full rounded-full py-3 px-6"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
value={question.correctAnswer.toLowerCase()}
|
||||
onChange={(e) =>
|
||||
handleQuestionChange(index, "correctAnswer", e.target.value)
|
||||
}
|
||||
placeholder="Correct answer (a, b, c, d)"
|
||||
className="bg-white w-full rounded-full py-3 px-6 mt-2"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={question.solution}
|
||||
onChange={(e) =>
|
||||
handleQuestionChange(index, "solution", e.target.value)
|
||||
}
|
||||
placeholder="Solution"
|
||||
className="bg-white w-full rounded-full py-3 px-6 mt-2"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={addQuestion}
|
||||
className="bg-green-500 text-white p-3 rounded-full mt-2"
|
||||
>
|
||||
Add Question
|
||||
</button>
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExamBuilder;
|
||||
|
||||
{
|
||||
/* */
|
||||
}
|
||||
28
examjam-admin/eslint.config.js
Normal file
@ -0,0 +1,28 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['dist'] },
|
||||
{
|
||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
)
|
||||
13
examjam-admin/index.html
Normal file
@ -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>ExamJam | Admin</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
32
examjam-admin/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "examjam-admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router": "^7.1.5",
|
||||
"tailwindcss": "^4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.14",
|
||||
"globals": "^15.12.0",
|
||||
"typescript": "~5.6.2",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"vite": "^6.0.1"
|
||||
}
|
||||
}
|
||||
15
examjam-admin/src/App.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import ExamBuilder from "../components/ExamBuilder";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<main className="bg-slate-900 h-screen flex flex-col items-center">
|
||||
<header className="flex w-full justify-between px-80 py-4 border-1 border-b-slate-500 bg-slate-200">
|
||||
<h1 className="text-xl font-semibold">ExamJam</h1>
|
||||
<button className="text-xl">Logout</button>
|
||||
</header>
|
||||
<ExamBuilder />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
1
examjam-admin/src/index.css
Normal file
@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
||||
15
examjam-admin/src/main.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
import { BrowserRouter, Route, Routes } from "react-router";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<App />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
);
|
||||
1
examjam-admin/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
26
examjam-admin/tsconfig.app.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
examjam-admin/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
examjam-admin/tsconfig.node.json
Normal file
@ -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"]
|
||||
}
|
||||
8
examjam-admin/vite.config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
});
|
||||
41
examjam-frontend/.gitignore
vendored
Normal file
@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
36
examjam-frontend/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||