From 01ea60c070a9eb97baac055a50bb9361a5e56434 Mon Sep 17 00:00:00 2001 From: DynoW <104089032+DynoW@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:19:13 +0300 Subject: [PATCH 1/7] Add new subjects --- .gitignore | 3 +- astro.config.mjs | 17 +- cuza-worker/src/app.ts | 105 ++++--- cuza-worker/src/index.ts | 9 + functions/api/[[route]].ts | 37 --- public/robots.txt | 2 +- src/api.ts | 7 +- src/components/Extra.astro | 54 ++-- src/components/Footer.astro | 2 +- src/components/List.astro | 103 ++++--- src/components/NewNavbar.astro | 13 +- src/components/NewPage.astro | 69 ++++- src/config/countdown.ts | 38 +-- src/config/labels.ts | 9 +- src/layouts/Layout.astro | 6 +- src/pages/404.astro | 7 +- src/pages/[subject]/[page].astro | 15 +- src/pages/[subject]/index.astro | 11 +- src/pages/contribute.astro | 11 +- src/pages/index.astro | 2 +- src/pages/install.astro | 11 +- src/pages/notite/info.astro | 8 +- src/pages/notite/romana.astro | 8 +- src/pages/upload.astro | 506 +++++++++++++------------------ src/paths.ts | 38 --- web-scraper/main.py | 121 +++++--- web-scraper/run.sh | 45 ++- 27 files changed, 636 insertions(+), 621 deletions(-) delete mode 100644 functions/api/[[route]].ts delete mode 100644 src/paths.ts diff --git a/.gitignore b/.gitignore index 5c734ff..e277443 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,4 @@ pnpm-debug.log* # temporary files web-scraper/temp/ -web-scraper/files/ -web-scraper/files2/ \ No newline at end of file +web-scraper/files* \ No newline at end of file diff --git a/astro.config.mjs b/astro.config.mjs index e4f6417..8032a6b 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -7,23 +7,11 @@ import playformCompress from '@playform/compress'; export default defineConfig({ site: 'https://cuza.pages.dev', trailingSlash: 'never', - redirects: { - '/fizica': '/', - }, build: { format: 'file', inlineStylesheets: 'always', - assets: 'file', }, vite: { - build: { - chunkSizeWarningLimit: 2000, - rollupOptions: { - output: { - assetFileNames: `file/[name][extname]`, - }, - }, - }, plugins: [tailwindcss()], }, markdown: { @@ -33,7 +21,10 @@ export default defineConfig({ }, integrations: [ sitemap({ - filter: (page) => !page.includes('/install') && !page.includes('/upload'), + filter: (page) => + !page.includes('/install') && + !page.includes('/upload') && + !page.includes('/contribute'), }), react(), playformCompress(), diff --git a/cuza-worker/src/app.ts b/cuza-worker/src/app.ts index 31a6bde..902cc80 100644 --- a/cuza-worker/src/app.ts +++ b/cuza-worker/src/app.ts @@ -108,6 +108,17 @@ function getSubtree( return current; } +/** + * Get a subtree as an object, returning {} if the path doesn't exist or is a leaf. + */ +function getSubtreeAsObject( + index: FileStructure, + segments: string[], +): FileStructure { + const subtree = getSubtree(index, segments); + return subtree !== null && typeof subtree !== 'string' ? subtree : {}; +} + /** * Set a value at a nested path, creating intermediate objects as needed. */ @@ -270,18 +281,34 @@ function pruneMissingIndexLeaves( return cleaned; } -const VALID_PAGES = new Set(['bac', 'teste', 'sim']); +const VALID_PAGES = new Set(['bac', 'sim']); const VALID_SIMULATIONS = new Set(['judetene', 'locale']); const VALID_TYPE2 = new Set(['var', 'bar']); -const BAC_TITLE_TO_CODE: Record = { - Model: 'SM', - Simulare: 'sim', - 'Sesiunea-I': 'S1', - 'Sesiunea-II': 'S2', - 'Sesiune-speciala': 'SS', -}; +const VALID_VARIANTS = new Set([ + '01', + '02', + '03', + '04', + '05', + '06', + '07', + '08', + '09', + '10', +]); +const VARIANT_REQUIRED_TITLES = new Set([ + 'Sesiunea-I', + 'Sesiunea-II', + 'Sesiunea-speciala', +]); +const BAC_TITLES = [ + 'Model', + 'Simulare', + 'Sesiunea-I', + 'Sesiunea-II', + 'Sesiunea-speciala', +] as const; const YEAR_RE = /^20[1-3]\d$/; -const TEST_NUMBER_RE = /^\d{1,2}$/; const MAX_FILE_BYTES = 20 * 1024 * 1024; // 20 MB /** @@ -319,20 +346,14 @@ function validateFormData(formData: FormData): string | null { // ── Page-specific validation ─────────────────────────────────────────────── if (page === 'bac') { const title = formData.get('title') as string | null; - if (!title) { - return 'Lipsă titlu'; - } - if (!(title in BAC_TITLE_TO_CODE)) { - return 'Titlu BAC invalid'; - } - } - if (page === 'teste') { - const testNumber = formData.get('testNumber') as string | null; - if (!testNumber) { - return 'Lipsă număr test'; + if (!title || !(BAC_TITLES as readonly string[]).includes(title)) { + return 'Lipsă tip sau tip invalid'; } - if (!TEST_NUMBER_RE.test(testNumber)) { - return 'Număr test invalid'; + if (VARIANT_REQUIRED_TITLES.has(title)) { + const variant = formData.get('variant') as string | null; + if (!variant || !VALID_VARIANTS.has(variant)) { + return 'Lipsă variantă sau variantă invalidă (01-10)'; + } } } if (page === 'sim') { @@ -358,37 +379,30 @@ interface UploadPathData { page: string; year: string; title: string | null; + variant: string | null; type2: string; reserve: boolean; - testNumber: string | null; simulation: string | null; county: string | null; local: string | null; } function generateUploadPath(data: UploadPathData): { r2Key: string } { - const { page, year, type2, testNumber, simulation, reserve } = data; + const { page, year, variant, simulation, reserve, type2 } = data; const title = sanitizePathSegment(data.title); const county = sanitizePathSegment(data.county); const local = sanitizePathSegment(data.local); if (page === 'bac') { - const baseCode = BAC_TITLE_TO_CODE[data.title ?? '']; - const code = reserve ? `${baseCode}R` : baseCode; - return { - r2Key: `fizica/pages/bac/${year}/${title}/E_d_fizica_${year}_${code}_${type2}.pdf`, - }; - } - if (page === 'teste') { - const testNumberNormalized = (testNumber ?? '').padStart(2, '0'); - const testType = type2 === 'bar' ? 'Bar' : 'Test'; + const folderName = reserve ? `${title}-rezerva` : title; + const varSuffix = variant ? `_${variant}` : '_00'; return { - r2Key: `fizica/pages/teste-de-antrenament/${year}/E_d_fizica_${year}_${testType}_${testNumberNormalized}.pdf`, + r2Key: `fizica/pages/teoretic/bac/${year}/${folderName}/E_d_fizica_teoretic_vocational_${year}_${type2}${varSuffix}.pdf`, }; } const location = simulation === 'judetene' ? county : local; return { - r2Key: `fizica/pages/simulari-judetene/${year}/E_d_fizica_${location}_${year}_${type2}.pdf`, + r2Key: `fizica/pages/teoretic/simulari-judetene/${year}/E_d_fizica_${location}_${year}_${type2}.pdf`, }; } @@ -410,23 +424,14 @@ export function registerRoutes(app: Hono<{ Bindings: Bindings }>): void { const index = await getIndex(c.env.FILES); const segments = resolvePathSegments(subject, page); - const subtree = getSubtree(index, segments); - const content: FileStructure = - subtree !== null && typeof subtree !== 'string' - ? (subtree as FileStructure) - : {}; + const content = getSubtreeAsObject(index, segments); const years = extractYears(content); - // Extra content const extraSegments = resolvePathSegments( subject, subject.toLowerCase() === 'admitere' ? `${page}/extra` : 'extra', ); - const extraSubtree = getSubtree(index, extraSegments); - const extra: FileStructure = - extraSubtree !== null && typeof extraSubtree !== 'string' - ? (extraSubtree as FileStructure) - : {}; + const extra = getSubtreeAsObject(index, extraSegments); return c.json({ content, extra, years }); }); @@ -475,25 +480,25 @@ export function registerRoutes(app: Hono<{ Bindings: Bindings }>): void { const page = formData.get('page') as string; const year = formData.get('year') as string; const title = formData.get('title') as string | null; - const type2 = formData.get('type2') as string; + const variant = formData.get('variant') as string | null; const reserveRaw = formData.get('reserve') as string | null; const reserve = reserveRaw === 'on' || reserveRaw === 'true'; - const testNumber = formData.get('testNumber') as string | null; const simulation = formData.get('simulation') as string | null; const county = formData.get('county') as string | null; const local = formData.get('local') as string | null; + const type2 = formData.get('type2') as string; const file = formData.get('file') as unknown as File; const { r2Key } = generateUploadPath({ page, year, title, - type2, + variant, reserve, - testNumber, simulation, county, local, + type2, }); await c.env.FILES.put(r2Key, file, { diff --git a/cuza-worker/src/index.ts b/cuza-worker/src/index.ts index 51b8185..211fb9c 100644 --- a/cuza-worker/src/index.ts +++ b/cuza-worker/src/index.ts @@ -16,6 +16,15 @@ app.use('*', async (c, next) => { return next(); }); +app.use('*', async (c, next) => { + await next(); + c.res.headers.set('X-Robots-Tag', 'noindex'); +}); + +app.get('/robots.txt', (c) => { + return c.text('User-agent: *\nDisallow: /\n'); +}); + app.use( '*', cors({ diff --git a/functions/api/[[route]].ts b/functions/api/[[route]].ts deleted file mode 100644 index df610dd..0000000 --- a/functions/api/[[route]].ts +++ /dev/null @@ -1,37 +0,0 @@ -/// - -export const onRequest: PagesFunction<{ FILES: R2Bucket }> = async (ctx) => { - const url = new URL(ctx.request.url); - const path = url.pathname.replace(/^\/api/, ''); - - if (path === '/ping') { - return new Response('Pong!', { status: 200 }); - } - - const fileMatch = path.match(/^\/file\/(.+)$/); - if (fileMatch) { - const key = fileMatch[1]; - const object = await ctx.env.FILES.get(key); - if (!object) return new Response('Not Found', { status: 404 }); - - const headers = new Headers(); - object.writeHttpMetadata(headers); - headers.set('etag', object.httpEtag); - headers.set('Cache-Control', 'public, max-age=31536000, immutable'); - - // Ensure PDFs are served inline so Googlebot can index them - const contentType = headers.get('Content-Type') || ''; - if ( - contentType.includes('application/pdf') || - key.toLowerCase().endsWith('.pdf') - ) { - headers.set('Content-Type', 'application/pdf'); - headers.set('Content-Disposition', 'inline'); - headers.set('X-Robots-Tag', 'index, follow'); - } - - return new Response(object.body, { headers }); - } - - return new Response('Not Found', { status: 404 }); -}; diff --git a/public/robots.txt b/public/robots.txt index 63bd157..49ac916 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,7 +1,7 @@ User-agent: * Allow: / -Allow: /api/file/ Disallow: /install Disallow: /upload +Disallow: /contribute Sitemap: https://cuza.pages.dev/sitemap-index.xml \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index 98c4d4f..63b292d 100644 --- a/src/api.ts +++ b/src/api.ts @@ -7,8 +7,11 @@ export class ApiService { constructor(baseUrl?: string) { this.baseUrl = - (baseUrl ?? import.meta.env.PUBLIC_WORKER_URL.replace(/\/$/, '')) || - DEFAULT_WORKER_URL; + baseUrl ?? + (import.meta.env.PUBLIC_WORKER_URL ?? DEFAULT_WORKER_URL).replace( + /\/$/, + '', + ); } private async fetchJson(url: string): Promise { diff --git a/src/components/Extra.astro b/src/components/Extra.astro index 963ccc6..1993563 100644 --- a/src/components/Extra.astro +++ b/src/components/Extra.astro @@ -14,28 +14,36 @@ interface Props { const { subject, page, extraData, years } = Astro.props as Props; const isAdmitere = subject === 'admitere'; ---- -

Extra

-{ - (subject === 'admitere' && page === 'fizica') || subject === 'fizica' ? ( - - Culegeri fizică - - ) : null -} -{ - isAdmitere ? ( - - ) : ( - - ) -} - - +const shouldFilter2007 = + (subject === 'fizica' && page === 'teoretic') || + (subject === 'mate' && page === 'mate-info'); +const yearsfix = shouldFilter2007 ? years?.filter((y) => y !== 2007) : years; +--- - +
+ { + Object.keys(extraData).length > 0 && ( + <> +

Extra

+ {(subject === 'admitere' && page === 'fizica') || + subject === 'fizica' ? ( + + Culegeri fizică + + ) : null} + {isAdmitere ? ( + + ) : ( + + )} + + ) + } + + +
diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 77ba366..be9f753 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -6,7 +6,7 @@ class="flex flex-col sm:flex-row justify-between items-center pt-3 gap-2" >

- Copyright © {new Date().getFullYear()} + Copyright © {new Date().getFullYear()}   { [/\bsesiunea\s*(ii|2)\b/, 3], [/\bsesiunea\s*(i|1)\b/, 4], [/(special|olimp)/, 5], - [/\bmodel\b/, 6], + [/\bbac\b/, 6], + [/\bmodel\b/, 7], + [/\bsimulari judetene\b/, 8], + [/\bteste de antrenament\b/, 9], + [/\b100 variante\b/, 10], ]; for (const [pattern, priority] of priorityRules) { @@ -41,52 +45,46 @@ const { isExtra: isExtraProp, } = Astro.props as Props; -const workerUrl = - import.meta.env.PUBLIC_WORKER_URL.replace(/\/$/, '') || - 'https://api.my-lab.ro'; +const baseUrl = ( + import.meta.env.PUBLIC_WORKER_URL ?? 'https://api.my-lab.ro' +).replace(/\/$/, ''); // Use explicit prop in recursive calls, derive from page name at root. const isExtra = isExtraProp ?? (page === 'extra' || page.endsWith('/extra')); const cx = isExtra ? 'extra' : 'content'; const paddingLeft = `${depth}rem`; -const sortedEntries = Object.entries(content).sort( - ([keyA, valA], [keyB, valB]) => { - const aIsYear = isYearFolder(keyA); - const bIsYear = isYearFolder(keyB); - const aIsFile = typeof valA === 'string'; - const bIsFile = typeof valB === 'string'; +const entries = Object.entries(content); +const sortedEntries = entries + .map(([key, value]) => ({ + key, + value, + isYear: isYearFolder(key), + isFile: typeof value === 'string', + fileMatch: + typeof value === 'string' ? key.match(/_(var|bar)_(\d{2})/i) : null, + priority: typeof value === 'string' ? 100 : getFolderPriority(key), + })) + .sort((a, b) => { + if (a.isYear && b.isYear) return Number(b.key) - Number(a.key); + if (a.isYear !== b.isYear) return a.isYear ? -1 : 1; - if (aIsYear && bIsYear) { - return Number(keyB) - Number(keyA); + if (a.isFile && b.isFile && a.fileMatch && b.fileMatch) { + const numDiff = parseInt(a.fileMatch[2]) - parseInt(b.fileMatch[2]); + if (numDiff !== 0) return numDiff; + const typeA = a.fileMatch[1].toLowerCase() === 'var' ? 0 : 1; + const typeB = b.fileMatch[1].toLowerCase() === 'var' ? 0 : 1; + return typeA - typeB; } - if (aIsYear !== bIsYear) { - return aIsYear ? -1 : 1; - } - - if (aIsFile && bIsFile) { - const matchA = keyA.match(/_(var|bar)_(\d{2})/i); - const matchB = keyB.match(/_(var|bar)_(\d{2})/i); - if (matchA && matchB) { - const numDiff = parseInt(matchA[2]) - parseInt(matchB[2]); - if (numDiff !== 0) return numDiff; - const typeA = matchA[1].toLowerCase() === 'var' ? 0 : 1; - const typeB = matchB[1].toLowerCase() === 'var' ? 0 : 1; - return typeA - typeB; - } - } + if (a.isFile !== b.isFile) return a.isFile ? 1 : -1; - if (aIsFile !== bIsFile) { - return aIsFile ? 1 : -1; - } - - const priorityDiff = getFolderPriority(keyA) - getFolderPriority(keyB); + const priorityDiff = a.priority - b.priority; if (priorityDiff !== 0) return priorityDiff; - return keyB.localeCompare(keyA); - }, -); + return b.key.localeCompare(a.key); + }) + .map(({ key, value }) => [key, value] as [string, FileStructure[string]]); ---

@@ -95,7 +93,10 @@ const sortedEntries = Object.entries(content).sort( sortedEntries.length > 0 ? (