mergin with dev #1

Closed
shafin808s wants to merge 0 commits from dev into main
283 changed files with 28732 additions and 6376 deletions

View File

@ -1,34 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "NextJS template",
description: "With typescript, tailwindCSS and shadcnUI",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View File

@ -1,31 +0,0 @@
import Image from "next/image";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>{" "}
template with typescript, tailwindCSS and shadcnUI
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
</main>
</div>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View 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

Submodule epaper added at 9d1aadf1d0

24
epaper-client/.gitignore vendored Normal file
View 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
View 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

Binary file not shown.

View File

@ -1,12 +1,12 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "slate",
"css": "src/index.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},

View 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
View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View 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
View 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;

View 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

View 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;

View 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;

View 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"
>
&times;
</button>
</div>
</div>
)}
</div>
);
};
export default NewspaperViewer;

View 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;

View 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 }

View 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 };

View 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,
}

View 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>;
}

View 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>;
}

View File

@ -4,112 +4,110 @@
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--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.129 0.042 264.695);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--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.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--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.984 0.003 247.858);
--sidebar-foreground: oklch(0.129 0.042 264.695);
--sidebar-primary: oklch(0.208 0.042 265.755);
--sidebar-primary-foreground: oklch(0.984 0.003 247.858);
--sidebar-accent: oklch(0.968 0.007 247.896);
--sidebar-accent-foreground: oklch(0.208 0.042 265.755);
--sidebar-border: oklch(0.929 0.013 255.508);
--sidebar-ring: oklch(0.704 0.04 256.788);
--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.129 0.042 264.695);
--foreground: oklch(0.984 0.003 247.858);
--card: oklch(0.208 0.042 265.755);
--card-foreground: oklch(0.984 0.003 247.858);
--popover: oklch(0.208 0.042 265.755);
--popover-foreground: oklch(0.984 0.003 247.858);
--primary: oklch(0.929 0.013 255.508);
--primary-foreground: oklch(0.208 0.042 265.755);
--secondary: oklch(0.279 0.041 260.031);
--secondary-foreground: oklch(0.984 0.003 247.858);
--muted: oklch(0.279 0.041 260.031);
--muted-foreground: oklch(0.704 0.04 256.788);
--accent: oklch(0.279 0.041 260.031);
--accent-foreground: oklch(0.984 0.003 247.858);
--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.551 0.027 264.364);
--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.208 0.042 265.755);
--sidebar-foreground: oklch(0.984 0.003 247.858);
--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.984 0.003 247.858);
--sidebar-accent: oklch(0.279 0.041 260.031);
--sidebar-accent-foreground: oklch(0.984 0.003 247.858);
--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.551 0.027 264.364);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
@ -120,3 +118,14 @@
@apply bg-background text-foreground;
}
}
@keyframes zoomIn {
from {
opacity: 0;
transform: scale(0.3);
}
to {
opacity: 1;
transform: scale(1);
}
}

View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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"]
}

View File

@ -0,0 +1,13 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

View 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"]
}

View 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",
];

View 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()
);
}

View 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
View 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
View 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

Binary file not shown.

View 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;

View 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;

View 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;

View 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;

View 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 },
],
},
},
]

View 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

File diff suppressed because it is too large Load Diff

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View 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

View 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;

View File

@ -0,0 +1,10 @@
@import "tailwindcss";
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}

View 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>
);

View 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"]
}

View 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",
];

View 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

Submodule examjam added at 264a25f614

24
examjam-admin/.gitignore vendored Normal file
View 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
View 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

Binary file not shown.

View 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;
{
/* */
}

View 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
View 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>

View 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
View 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;

View File

@ -0,0 +1 @@
@import "tailwindcss";

View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View 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"]
}

View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View 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"]
}

View 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()],
});

Some files were not shown because too many files have changed in this diff Show More