From 255ac8cb40ddbb1d9f858af336671328ed694c9c Mon Sep 17 00:00:00 2001 From: "ar.badiei" Date: Mon, 20 Jul 2026 20:36:35 +0330 Subject: [PATCH] Initial commit: Feep Block pricing calculator with page multiplier Co-authored-by: Cursor --- .gitignore | 6 + calculator.js | 2679 +++++++++++++++++++++++++++++++++++++++++++++++++ feep-logo.svg | 28 + index.html | 2431 ++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 + reui-logo.svg | 1 + reui-temp.css | 25 + server.js | 155 +++ 8 files changed, 5338 insertions(+) create mode 100644 .gitignore create mode 100644 calculator.js create mode 100644 feep-logo.svg create mode 100644 index.html create mode 100644 package.json create mode 100644 reui-logo.svg create mode 100644 reui-temp.css create mode 100644 server.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9689e30 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.DS_Store +Thumbs.db +*.log +.env +.env.* diff --git a/calculator.js b/calculator.js new file mode 100644 index 0000000..ee368fa --- /dev/null +++ b/calculator.js @@ -0,0 +1,2679 @@ +(function () { + 'use strict'; + + const UNIT_PRICE = 500000; + + const REUSE = { + new_block: 1, + no_change: 0.10, + minor: 0.25, + medium: 0.40, + major: 0.70 + }; + + const REUSE_ONLY = { + no_change: 0.10, + minor: 0.25, + medium: 0.40, + major: 0.70 + }; + + const CATALOG = { + general: { + label: 'عمومی', + hasComplexity: true, + blocks: { + Navbar: { simple: 1, medium: 1.5, complex: 2 }, + Sidebar: { simple: 1, medium: 2, complex: 3 }, + Footer: { simple: 0.8, medium: 1, complex: 1.5 }, + Hero: { simple: 2, medium: 3, complex: 4 }, + Banner: { simple: 1, medium: 1.5, complex: 2 }, + CTA: { simple: 0.8, medium: 1, complex: 1.5 }, + 'Feature Grid': { simple: 1, medium: 2, complex: 3 }, + 'Card Section': { simple: 1, medium: 2, complex: 3 }, + FAQ: { simple: 1, medium: 1.5, complex: 2 }, + Timeline: { simple: 1, medium: 2, complex: 3 }, + Testimonial: { simple: 1, medium: 2, complex: 3 }, + Pricing: { simple: 2, medium: 3, complex: 4 }, + Gallery: { simple: 2, medium: 3, complex: 5 }, + 'Blog List': { simple: 2, medium: 3, complex: 4 }, + 'Contact Section': { simple: 1, medium: 2, complex: 3 } + } + }, + dashboard: { + label: 'داشبورد', + hasComplexity: true, + blocks: { + 'Stat Card': { simple: 0.5, medium: 0.8, complex: 1 }, + Widget: { simple: 1, medium: 2, complex: 3 }, + 'Data Table': { simple: 2, medium: 4, complex: 6 }, + 'Filter Bar': { simple: 1, medium: 2, complex: 3 }, + Search: { simple: 0.5, medium: 1, complex: 1.5 }, + Pagination: { simple: 0.5, medium: 0.8, complex: 1 }, + Tabs: { simple: 0.8, medium: 1, complex: 2 }, + Chart: { simple: 2, medium: 3, complex: 5 }, + Calendar: { simple: 2, medium: 4, complex: 6 }, + Kanban: { simple: 4, medium: 6, complex: 8 } + } + }, + form: { + label: 'فرم', + hasComplexity: true, + blocks: { + 'Form Section': { simple: 1, medium: 2, complex: 3 }, + 'Input Group': { simple: 0.3, medium: 0.5, complex: 1 }, + 'File Upload': { simple: 1, medium: 2, complex: 3 }, + 'Rich Text': { simple: 2, medium: 3, complex: 4 }, + 'Multi Step': { simple: 3, medium: 5, complex: 8 }, + 'Dynamic Field': { simple: 2, medium: 4, complex: 6 } + } + }, + interactive: { + label: 'تعاملی', + hasComplexity: false, + blocks: { + Modal: 1, + Drawer: 1, + Dropdown: 0.5, + Popover: 0.5, + 'Command Menu': 1, + Toast: 0.5, + Skeleton: 0.5, + 'Loading State': 0.5, + 'Empty State': 0.5, + 'Error State': 0.5 + } + } + }; + + const COMPLEXITY_LABELS = { simple: 'Simple', medium: 'Medium', complex: 'Complex' }; + const REUSE_LABELS = { + new_block: 'جدید (۱۰۰٪)', + no_change: 'Reuse بدون تغییر (۱۰٪)', + minor: 'تغییر جزئی (۲۵٪)', + medium: 'تغییر متوسط (۴۰٪)', + major: 'تغییر زیاد (۷۰٪)' + }; + const REUSE_ONLY_LABELS = { + no_change: 'بدون تغییر (۱۰٪)', + minor: 'تغییر جزئی (۲۵٪)', + medium: 'تغییر متوسط (۴۰٪)', + major: 'تغییر زیاد (۷۰٪)' + }; + const CAT_LABELS = { general: 'عمومی', dashboard: 'داشبورد', form: 'فرم', interactive: 'تعاملی' }; + + const DEFAULT_ROWS = []; + + const LAYOUT_BLOCKS = { Navbar: true, Footer: true }; + + const BLOCK_SIMILARITY_GROUPS = [ + ['Hero', 'Banner', 'CTA'], + ['Feature Grid', 'Card Section', 'Gallery', 'Pricing'], + ['Testimonial', 'FAQ', 'Timeline'], + ['Blog List', 'Card Section', 'Feature Grid', 'Gallery'], + ['Contact Section', 'Form Section', 'CTA', 'Banner'], + ['Navbar', 'Sidebar'], + ['Stat Card', 'Widget', 'Chart'], + ['Data Table', 'Filter Bar', 'Search', 'Pagination', 'Tabs'], + ['Modal', 'Drawer', 'Popover', 'Dropdown', 'Toast', 'Command Menu'], + ['Form Section', 'Input Group', 'File Upload', 'Multi Step', 'Dynamic Field'] + ]; + + const REUSE_MATCH_THRESHOLD = 18; + + /** @type {Record} */ + var urlAnalysisCache = Object.create(null); + + let pageIdCounter = 0; + let rowIdCounter = 0; + let rowsEl, emptyEl, tabsEl, pagesListEl, archiveListEl, calcMainEl; + let activePageId = null; + let isRendering = false; + + const state = { + pages: [], + reportEdited: false + }; + + function escapeHtml(str) { + return String(str == null ? '' : str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function getReportBodyEl() { + return document.getElementById('calc-project-report-body'); + } + + function getProjectTitle() { + var el = document.getElementById('opt-project-title'); + return el ? el.value.trim() : ''; + } + + function buildReportHeading() { + var title = getProjectTitle(); + if (title) return 'گزارش برآورد پروژه — ' + title; + return 'گزارش برآورد پروژه'; + } + + function sanitizeFilename(name) { + return String(name) + .replace(/[^\w\u0600-\u06FF\-]+/g, '-') + .replace(/^-+|-+$/g, '') + .slice(0, 60) || 'project'; + } + + function getReportDownloadBasename() { + var title = getProjectTitle(); + var stamp = new Date().toISOString().slice(0, 10); + if (title) return 'project-report-' + sanitizeFilename(title) + '-' + stamp; + return 'project-report-' + stamp; + } + + function saveProjectTitleToStorage() { + try { + localStorage.setItem('feep-price-project-title', getProjectTitle()); + } catch (e) { /* ignore quota */ } + } + + function loadProjectTitleFromStorage() { + var el = document.getElementById('opt-project-title'); + if (!el) return; + try { + var saved = localStorage.getItem('feep-price-project-title'); + if (saved != null) el.value = saved; + } catch (e) { /* ignore */ } + } + + function nextPageId() { + pageIdCounter += 1; + return pageIdCounter; + } + + function nextRowId() { + rowIdCounter += 1; + return rowIdCounter; + } + + function fmt(n) { + return Math.round(n).toLocaleString('fa-IR'); + } + + function fmtUnit(n) { + const s = n % 1 === 0 ? n.toFixed(0) : n.toFixed(1); + return parseFloat(s).toLocaleString('fa-IR', { maximumFractionDigits: 1 }); + } + + function getBlockUnit(catKey, blockName, complexity) { + const cat = CATALOG[catKey]; + if (!cat) return 0; + const block = cat.blocks[blockName]; + if (block == null) return 0; + if (!cat.hasComplexity) return block; + return block[complexity] ?? 0; + } + + function buildOptions(obj, labels) { + return Object.keys(obj).map(function (k) { + return ''; + }).join(''); + } + + function getActivePage() { + return state.pages.find(function (p) { return p.id === activePageId; }); + } + + function getPageIndex(pageId) { + return state.pages.findIndex(function (p) { return p.id === pageId; }); + } + + function createPageData(name, rows) { + return { + id: nextPageId(), + name: name, + archived: false, + rows: (rows || []).map(function (r) { + return Object.assign({ id: nextRowId() }, r); + }), + animation: '0', + animUnits: 2, + pageMultiplier: 1 + }; + } + + function parsePageMultiplier(val) { + var n = parseInt(val, 10); + if (!n || n < 1) return 1; + return Math.min(n, 999); + } + + function getPageMultiplier(page) { + if (!page) return 1; + return parsePageMultiplier(page.pageMultiplier); + } + + function getDraftPages() { + return state.pages.filter(function (p) { return !p.archived; }); + } + + function getArchivedPages() { + return state.pages.filter(function (p) { return p.archived; }); + } + + function isPageReadOnly(page) { + return page && page.archived; + } + + function getNextDraftPageName() { + var n = state.pages.length + 1; + return n === 1 ? 'Home' : 'صفحه ' + n; + } + + function rowDataFromDom(rowEl) { + var data = { + id: parseInt(rowEl.dataset.id, 10), + mode: rowEl.dataset.mode || 'new', + cat: rowEl.querySelector('.sel-cat').value, + block: rowEl.querySelector('.sel-block').value, + complexity: rowEl.querySelector('.sel-complexity').value, + reuse: rowEl.querySelector('.sel-reuse').value + }; + if (data.mode === 'reuse') { + data.sourcePageId = parseInt(rowEl.dataset.sourcePageId, 10); + data.sourceRowId = parseInt(rowEl.dataset.sourceRowId, 10); + if (rowEl.dataset.reuseMatchType) data.reuseMatchType = rowEl.dataset.reuseMatchType; + } + return data; + } + + function saveActivePageFromDom() { + var page = getActivePage(); + if (!page || !rowsEl) return; + + if (!page.archived) { + page.name = document.getElementById('calc-page-name').value.trim() || page.name; + page.animation = document.getElementById('opt-animation').value; + page.animUnits = parseFloat(document.getElementById('opt-anim-units').value) || 2; + } + var multEl = document.getElementById('opt-page-multiplier'); + if (multEl) page.pageMultiplier = parsePageMultiplier(multEl.value); + page.rows = Array.from(rowsEl.querySelectorAll('.calc-row')).map(rowDataFromDom); + sanitizePageLayoutRows(page); + } + + function getAnimUnitsForPage(page) { + if (page.animation === 'complex') { + return Math.min(3, Math.max(1, page.animUnits || 2)); + } + return parseFloat(page.animation) || 0; + } + + function calcRowUnits(rowData) { + var base = getBlockUnit(rowData.cat, rowData.block, rowData.complexity); + var reuseKey = rowData.mode === 'reuse' ? rowData.reuse : rowData.reuse; + var factor = REUSE[reuseKey] != null ? REUSE[reuseKey] : 1; + return base * factor; + } + + function calcPageTotals(page) { + var blockUnits = 0; + getRowsForPage(page).forEach(function (row) { + blockUnits += calcRowUnits(row); + }); + var animUnits = getAnimUnitsForPage(page); + var baseTotalUnits = blockUnits + animUnits; + var mult = getPageMultiplier(page); + var totalUnits = baseTotalUnits * mult; + return { + blockUnits: blockUnits * mult, + animUnits: animUnits * mult, + totalUnits: totalUnits, + cost: totalUnits * UNIT_PRICE, + pageMultiplier: mult, + baseTotalUnits: baseTotalUnits + }; + } + + function getBlockRegistry() { + var registry = []; + var seen = Object.create(null); + getArchivedPages().forEach(function (page) { + page.rows.forEach(function (row) { + if (row.mode === 'reuse') return; + if (row.reuse !== 'new_block') return; + var key = row.cat + '::' + row.block; + if (seen[key]) return; + seen[key] = true; + registry.push({ + sourcePageId: page.id, + sourceRowId: row.id, + pageName: page.name, + cat: row.cat, + block: row.block, + complexity: row.complexity, + label: row.label || row.block, + fingerprint: row.fingerprint || '', + key: key + }); + }); + }); + return registry; + } + + function getSequentialPageNumber() { + return getArchivedPages().length + 1; + } + + function normalizeImportUrl(url) { + try { + var parsed = new URL(url); + var host = parsed.hostname.replace(/^www\./i, '').toLowerCase(); + var path = parsed.pathname.replace(/\/+$/, '') || '/'; + return parsed.protocol + '//' + host + path + (parsed.search || ''); + } catch (e) { + return (url || '').trim().toLowerCase(); + } + } + + function findPageBySourceUrl(url) { + var key = normalizeImportUrl(url); + return state.pages.find(function (p) { + return p.sourceUrl && normalizeImportUrl(p.sourceUrl) === key; + }) || null; + } + + function getCachedUrlAnalysis(url) { + return urlAnalysisCache[normalizeImportUrl(url)] || null; + } + + function cacheUrlAnalysis(url, parsed) { + urlAnalysisCache[normalizeImportUrl(url)] = { + rows: parsed.rows.map(function (r) { return Object.assign({}, r); }), + pageTitle: parsed.pageTitle, + meta: Object.assign({}, parsed.meta || {}), + analyzed: parsed.analyzed ? parsed.analyzed.map(function (a) { return Object.assign({}, a); }) : undefined + }; + } + + function resolveUrlAnalysis(url, html) { + var cached = getCachedUrlAnalysis(url); + if (cached) { + return { + rows: cached.rows.map(function (r) { return Object.assign({}, r); }), + pageTitle: cached.pageTitle, + meta: Object.assign({}, cached.meta), + analyzed: cached.analyzed ? cached.analyzed.map(function (a) { return Object.assign({}, a); }) : undefined + }; + } + var parsed = parseBlocksFromHtml(html); + cacheUrlAnalysis(url, parsed); + return parsed; + } + + function isLayoutBlock(blockName) { + return !!LAYOUT_BLOCKS[blockName]; + } + + function getProjectPageNumber(page) { + if (!page) return 1; + if (page.projectPageNumber) return page.projectPageNumber; + var archived = getArchivedPages(); + var idx = archived.findIndex(function (p) { return p.id === page.id; }); + if (idx >= 0) return idx + 1; + return archived.length + 1; + } + + function shouldExcludeLayoutForPage(page) { + return getProjectPageNumber(page) > 1; + } + + function filterLayoutRows(rows) { + return rows.filter(function (row) { + return !isLayoutBlock(row.block); + }); + } + + function getRowsForPage(page) { + if (!page || !page.rows) return []; + if (!shouldExcludeLayoutForPage(page)) return page.rows; + return filterLayoutRows(page.rows); + } + + function sanitizePageLayoutRows(page) { + if (!page || !page.rows) return; + if (!shouldExcludeLayoutForPage(page)) return; + page.rows = filterLayoutRows(page.rows); + } + + function getReuseRegistryForPage(page) { + var registry = getBlockRegistry(); + if (!shouldExcludeLayoutForPage(page)) return registry; + return registry.filter(function (r) { return !isLayoutBlock(r.block); }); + } + + function getBlockSimilarityGroup(blockName) { + for (var i = 0; i < BLOCK_SIMILARITY_GROUPS.length; i++) { + if (BLOCK_SIMILARITY_GROUPS[i].indexOf(blockName) !== -1) { + return BLOCK_SIMILARITY_GROUPS[i]; + } + } + return null; + } + + function blocksAreSimilar(blockA, blockB) { + if (!blockA || !blockB) return false; + if (blockA === blockB) return true; + var group = getBlockSimilarityGroup(blockA); + return group ? group.indexOf(blockB) !== -1 : false; + } + + function tokenizeForSimilarity(str) { + return (str || '').toLowerCase() + .replace(/[^\u0600-\u06FFa-z0-9\s]/g, ' ') + .split(/\s+/) + .filter(function (t) { return t.length > 2; }); + } + + function textSimilarity(a, b) { + var tokensA = tokenizeForSimilarity(a); + var tokensB = tokenizeForSimilarity(b); + if (!tokensA.length || !tokensB.length) return 0; + var overlap = 0; + tokensA.forEach(function (t) { + if (tokensB.indexOf(t) !== -1) overlap += 1; + }); + return overlap / Math.max(tokensA.length, tokensB.length); + } + + function fingerprintSimilarity(a, b) { + if (!a || !b) return 0; + if (a === b) return 1; + var partsA = a.split('|').filter(Boolean); + var partsB = b.split('|').filter(Boolean); + if (!partsA.length || !partsB.length) return textSimilarity(a, b); + var overlap = 0; + partsA.forEach(function (p) { + if (partsB.indexOf(p) !== -1) overlap += 1; + }); + return overlap / Math.max(partsA.length, partsB.length); + } + + function scoreReuseMatch(candidate, cat, block, meta) { + var score = 0; + var matchType = 'similar'; + + if (candidate.key === cat + '::' + block) { + score += 100; + matchType = 'exact'; + } + if (blocksAreSimilar(block, candidate.block)) { + score += 45; + if (matchType !== 'exact') matchType = 'similar-type'; + } + if (candidate.cat === cat) { + score += 22; + if (score < 45) matchType = 'same-category'; + } + if (meta && meta.fingerprint && candidate.fingerprint) { + var fpScore = fingerprintSimilarity(meta.fingerprint, candidate.fingerprint); + if (fpScore >= 0.4) { + score += Math.round(fpScore * 35); + if (matchType === 'similar') matchType = 'fingerprint'; + } + } + if (meta && meta.label && candidate.label) { + var labelScore = textSimilarity(meta.label, candidate.label); + if (labelScore >= 0.35) { + score += Math.round(labelScore * 25); + } + } + + return { score: score, matchType: matchType }; + } + + function buildRegistryEntry(sourcePageId, sourceRowId, pageName, row) { + return { + sourcePageId: sourcePageId, + sourceRowId: sourceRowId, + pageName: pageName, + cat: row.cat, + block: row.block, + complexity: row.complexity, + label: row.label || row.block, + fingerprint: row.fingerprint || '', + key: row.cat + '::' + row.block + }; + } + + function findReuseInRegistry(registry, cat, block, meta) { + if (!registry.length) return null; + + var best = null; + var bestScore = REUSE_MATCH_THRESHOLD; + var bestType = 'similar'; + + registry.forEach(function (candidate) { + var scored = scoreReuseMatch(candidate, cat, block, meta || {}); + if (scored.score > bestScore) { + bestScore = scored.score; + best = candidate; + bestType = scored.matchType; + } else if (scored.score === bestScore && best) { + if (candidate.sourcePageId < best.sourcePageId || + (candidate.sourcePageId === best.sourcePageId && candidate.sourceRowId < best.sourceRowId)) { + best = candidate; + bestType = scored.matchType; + } + } + }); + + if (!best) return null; + return { source: best, matchType: bestType, score: bestScore }; + } + + function findIntraPageReuse(localRegistry, cat, block, meta) { + if (!localRegistry.length) return null; + + for (var i = 0; i < localRegistry.length; i++) { + var candidate = localRegistry[i]; + if (candidate.cat === cat && candidate.block === block) { + return { source: candidate, matchType: 'exact', score: 100 }; + } + } + + if (meta && meta.fingerprint) { + for (var j = 0; j < localRegistry.length; j++) { + var fpCandidate = localRegistry[j]; + if (fpCandidate.fingerprint && fpCandidate.fingerprint === meta.fingerprint) { + return { source: fpCandidate, matchType: 'exact', score: 100 }; + } + } + } + + return findReuseInRegistry(localRegistry, cat, block, meta); + } + + function ensureLocalRegistryEntry(localRegistry, resultIndex, row) { + var key = row.cat + '::' + row.block; + var hasKey = localRegistry.some(function (c) { return c.key === key; }); + var hasFingerprint = row.fingerprint && localRegistry.some(function (c) { + return c.fingerprint && c.fingerprint === row.fingerprint; + }); + if (hasKey || hasFingerprint) return; + + localRegistry.push(buildRegistryEntry( + INTRA_PAGE_SOURCE, + resultIndex, + 'همین صفحه', + row + )); + } + + function findReuseSource(cat, block, meta) { + return findReuseInRegistry(getBlockRegistry(), cat, block, meta); + } + + var INTRA_PAGE_SOURCE = '__same_page__'; + + function resolveIntraPageReuseReferences(page) { + if (!page || !page.rows) return; + var indexToId = page.rows.map(function (r) { return r.id; }); + page.rows.forEach(function (row) { + if (row.sourcePageId !== INTRA_PAGE_SOURCE) return; + var idx = row.sourceRowId; + if (typeof idx === 'number' && indexToId[idx] != null) { + row.sourcePageId = page.id; + row.sourceRowId = indexToId[idx]; + row.sourcePageName = page.name; + } + }); + } + + function dedupeRowsWithinPage(page) { + if (!page || !page.rows || page.rows.length < 2) return 0; + + var localRegistry = []; + var reuseCount = 0; + + page.rows.forEach(function (row) { + var meta = { + fingerprint: row.fingerprint || '', + label: row.label || row.block + }; + + if (row.mode === 'reuse') { + var key = row.cat + '::' + row.block; + var hasKey = localRegistry.some(function (c) { return c.key === key; }); + var hasFingerprint = row.fingerprint && localRegistry.some(function (c) { + return c.fingerprint && c.fingerprint === row.fingerprint; + }); + if (!hasKey && !hasFingerprint) { + localRegistry.push(buildRegistryEntry(page.id, row.id, page.name, row)); + } + return; + } + + if (row.reuse !== 'new_block') return; + + var match = findIntraPageReuse(localRegistry, row.cat, row.block, meta); + + if (match) { + row.mode = 'reuse'; + row.sourcePageId = page.id; + row.sourceRowId = match.source.sourceRowId; + row.sourcePageName = page.name; + row.reuse = reuseLevelForMatch(match.matchType); + row.reuseMatchType = match.matchType; + reuseCount += 1; + } else { + localRegistry.push(buildRegistryEntry(page.id, row.id, page.name, row)); + } + }); + + return reuseCount; + } + + function reuseLevelForMatch(matchType) { + if (matchType === 'exact') return 'no_change'; + if (matchType === 'similar-type' || matchType === 'fingerprint') return 'minor'; + return 'medium'; + } + + function matchRowsAsNew(rawRows, options) { + return matchRowsWithReuse(rawRows, Object.assign({}, options || {}, { allowArchiveReuse: false })); + } + + function matchRowsWithReuse(rawRows, options) { + var excludeLayout = options && options.excludeLayout; + var excludeSourcePageId = options && options.excludeSourcePageId; + var allowArchiveReuse = !options || options.allowArchiveReuse !== false; + var result = []; + var localRegistry = []; + var stats = { newCount: 0, reuseCount: 0, skippedLayout: 0 }; + + rawRows.forEach(function (row) { + if (excludeLayout && isLayoutBlock(row.block)) { + stats.skippedLayout += 1; + return; + } + + var meta = { + fingerprint: row.fingerprint, + label: row.label + }; + + var match = findIntraPageReuse(localRegistry, row.cat, row.block, meta); + if (!match && allowArchiveReuse) { + match = findReuseSource(row.cat, row.block, meta); + if (match && excludeSourcePageId && match.source.sourcePageId === excludeSourcePageId) { + match = null; + } + } + + if (match) { + result.push({ + mode: 'reuse', + sourcePageId: match.source.sourcePageId, + sourceRowId: match.source.sourceRowId, + sourcePageName: match.source.pageName, + cat: row.cat, + block: row.block, + complexity: row.complexity || match.source.complexity, + reuse: reuseLevelForMatch(match.matchType), + fingerprint: row.fingerprint || '', + label: row.label || row.block, + reuseMatchType: match.matchType + }); + stats.reuseCount += 1; + ensureLocalRegistryEntry(localRegistry, result.length - 1, row); + } else { + result.push({ + mode: 'new', + cat: row.cat, + block: row.block, + complexity: row.complexity, + reuse: 'new_block', + fingerprint: row.fingerprint || '', + label: row.label || row.block + }); + ensureLocalRegistryEntry(localRegistry, result.length - 1, row); + stats.newCount += 1; + } + }); + + return { rows: result, stats: stats }; + } + + function getContentPercent() { + var v = document.getElementById('opt-content').value; + if (v === '0.05_img') return 0.05; + return parseFloat(v) || 0; + } + + function getRevisionUnits() { + var el = document.getElementById('opt-revision-units'); + if (!el) return 0; + var n = parseFloat(el.value); + return Math.max(0, n || 0); + } + + function getSelectLabel(id) { + var el = document.getElementById(id); + if (!el || !el.options.length) return '—'; + return el.options[el.selectedIndex].textContent.trim(); + } + + function getAnimationLabel(page) { + if (!page) return '—'; + if (page.animation === 'complex') { + return 'Motion پیچیده — ' + fmtUnit(getAnimUnitsForPage(page)) + ' Unit'; + } + if (page.animation === '0') return 'Hover Effect — رایگان'; + if (page.animation === '0.2') return 'Transition — ۰.۲ Unit'; + if (page.animation === '0.5') return 'Motion ساده — ۰.۵ Unit'; + return '—'; + } + + function getReuseStatusLabel(row) { + if (row.mode === 'reuse') { + return REUSE_ONLY_LABELS[row.reuse] || row.reuse; + } + return REUSE_LABELS[row.reuse] || REUSE_LABELS.new_block; + } + + function getProjectFinancials() { + var projectBlockUnits = 0; + var projectAnimUnits = 0; + + state.pages.forEach(function (page) { + var totals = calcPageTotals(page); + projectBlockUnits += totals.blockUnits; + projectAnimUnits += totals.animUnits; + }); + + var revisionUnits = getRevisionUnits(); + var totalUnits = projectBlockUnits + projectAnimUnits + revisionUnits; + var blocksCost = totalUnits * UNIT_PRICE; + + var contentPct = getContentPercent(); + var responsivePct = parseFloat(document.getElementById('opt-responsive').value) || 0; + var darkPct = parseFloat(document.getElementById('opt-dark').value) || 0; + var bilingualPct = parseFloat(document.getElementById('opt-bilingual').value) || 0; + + var revisionCost = revisionUnits * UNIT_PRICE; + var contentExtra = blocksCost * contentPct; + var afterContent = blocksCost + contentExtra; + var responsiveExtra = afterContent * responsivePct; + var afterResponsive = afterContent + responsiveExtra; + var darkExtra = afterResponsive * darkPct; + var afterDark = afterResponsive + darkExtra; + var bilingualExtra = afterDark * bilingualPct; + var total = afterDark + bilingualExtra; + + return { + projectBlockUnits: projectBlockUnits, + projectAnimUnits: projectAnimUnits, + revisionUnits: revisionUnits, + totalUnits: totalUnits, + blocksCost: blocksCost, + contentExtra: contentExtra, + responsiveExtra: responsiveExtra, + darkExtra: darkExtra, + bilingualExtra: bilingualExtra, + revisionCost: revisionCost, + total: total + }; + } + + function formatReportDate(date) { + try { + return date.toLocaleString('fa-IR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch (e) { + return date.toISOString(); + } + } + + function getRowReportDetail(row) { + var complexity = CATALOG[row.cat] && CATALOG[row.cat].hasComplexity + ? COMPLEXITY_LABELS[row.complexity] || row.complexity + : '—'; + var status = row.mode === 'reuse' ? 'Reuse' : 'جدید'; + return complexity + ' · ' + status; + } + + function reportKvTable(rows) { + var html = ''; + rows.forEach(function (row) { + html += ''; + }); + html += '
' + escapeHtml(row[0]) + '' + escapeHtml(row[1]) + '
'; + return html; + } + + function reportBlocksTableColgroup() { + return '' + + '' + + '' + + '' + + '' + + ''; + } + + function buildProjectReportHtml() { + var archived = getArchivedPages(); + var drafts = getDraftPages(); + var finances = getProjectFinancials(); + var now = new Date(); + var parts = []; + + parts.push('
'); + parts.push('

' + escapeHtml(buildReportHeading()) + '

'); + + var metaRows = [ + ['عنوان پروژه', getProjectTitle() || '—'], + ['تاریخ گزارش', formatReportDate(now)], + ['تعرفه هر Unit', fmt(UNIT_PRICE) + ' تومان'], + ['صفحات تأییدشده', archived.length.toLocaleString('fa-IR')] + ]; + if (drafts.length) { + metaRows.push(['پیش‌نویس‌های باز', drafts.length.toLocaleString('fa-IR') + ' — ' + drafts.map(function (p) { return p.name; }).join('، ')]); + } + parts.push(reportKvTable(metaRows)); + + parts.push('

گزینه‌های پروژه

'); + parts.push(reportKvTable([ + ['محتوا', getSelectLabel('opt-content')], + ['Responsive', getSelectLabel('opt-responsive')], + ['Dark Mode', getSelectLabel('opt-dark')], + ['دوزبانه', getSelectLabel('opt-bilingual')], + ['Revision', finances.revisionUnits ? fmtUnit(finances.revisionUnits) + ' Unit' : 'ندارد'] + ])); + + if (!archived.length) { + parts.push('

هنوز صفحه‌ای آرشیو نشده — بعد از تحلیل URL، صفحه را آرشیو کنید.

'); + } else { + parts.push('

بلاک‌های صفحات

'); + parts.push('' + reportBlocksTableColgroup()); + parts.push(''); + ['Block', 'جزئیات', 'Unit', 'هزینه (تومان)'].forEach(function (h) { + parts.push(''); + }); + parts.push(''); + + archived.forEach(function (page, pageIdx) { + var totals = calcPageTotals(page); + var pageLabel = 'صفحه ' + (pageIdx + 1).toLocaleString('fa-IR') + ': ' + page.name; + if (totals.pageMultiplier > 1) { + pageLabel += ' (ضریب ×' + totals.pageMultiplier.toLocaleString('fa-IR') + ' — ' + + totals.pageMultiplier.toLocaleString('fa-IR') + ' صفحه مشابه)'; + } + if (page.sourceUrl) pageLabel += ' — ' + page.sourceUrl; + + parts.push(''); + + var rows = getRowsForPage(page); + if (!rows.length) { + parts.push(''); + } else { + rows.forEach(function (row) { + var units = calcRowUnits(row); + var cost = units * UNIT_PRICE; + var blockLabel = row.block; + if (row.label && row.label !== row.block) blockLabel += ' — ' + row.label; + parts.push(''); + parts.push(''); + parts.push(''); + parts.push(''); + parts.push(''); + parts.push(''); + }); + } + + parts.push(''); + var subtotalLabel = 'جمع «' + page.name + '» — ' + getAnimationLabel(page); + if (totals.pageMultiplier > 1) { + subtotalLabel += ' — ضریب ×' + totals.pageMultiplier.toLocaleString('fa-IR') + + ' (' + fmtUnit(totals.baseTotalUnits) + ' × ' + totals.pageMultiplier.toLocaleString('fa-IR') + ')'; + } + parts.push(''); + parts.push(''); + parts.push(''); + parts.push(''); + }); + + parts.push('
' + escapeHtml(h) + '
' + escapeHtml(pageLabel) + '
(بدون بلاک)
' + escapeHtml(blockLabel) + '' + escapeHtml(getRowReportDetail(row)) + '' + escapeHtml(fmtUnit(units)) + '' + escapeHtml(fmt(cost)) + '
' + escapeHtml(subtotalLabel) + '' + escapeHtml(fmtUnit(totals.totalUnits)) + '' + escapeHtml(fmt(totals.cost)) + '
'); + } + + parts.push('

خلاصه مالی

'); + var summaryRows = [ + ['Unit بلاک‌ها', fmtUnit(finances.projectBlockUnits)], + ['Unit انیمیشن', fmtUnit(finances.projectAnimUnits)] + ]; + if (finances.revisionUnits) summaryRows.push(['Revision', fmtUnit(finances.revisionUnits) + ' Unit']); + summaryRows.push(['مجموع Unit', fmtUnit(finances.totalUnits)]); + summaryRows.push(['هزینه Blockها', fmt(finances.blocksCost) + ' تومان']); + if (finances.contentExtra) summaryRows.push(['اضافه محتوا', fmt(finances.contentExtra) + ' تومان']); + if (finances.responsiveExtra) summaryRows.push(['اضافه Responsive', fmt(finances.responsiveExtra) + ' تومان']); + if (finances.darkExtra) summaryRows.push(['اضافه Dark Mode', fmt(finances.darkExtra) + ' تومان']); + if (finances.bilingualExtra) summaryRows.push(['اضافه دوزبانه', fmt(finances.bilingualExtra) + ' تومان']); + if (finances.revisionCost) summaryRows.push(['هزینه Revision', fmt(finances.revisionCost) + ' تومان']); + summaryRows.push(['قیمت نهایی', fmt(finances.total) + ' تومان']); + parts.push(reportKvTable(summaryRows)); + parts.push('
'); + + return parts.join(''); + } + + function buildProjectReportText() { + var body = getReportBodyEl(); + if (body && body.innerText) return body.innerText.trim(); + var tmp = document.createElement('div'); + tmp.innerHTML = buildProjectReportHtml(); + return tmp.innerText.trim(); + } + + function saveReportToStorage() { + var body = getReportBodyEl(); + if (!body) return; + try { + localStorage.setItem('feep-price-report-html', body.innerHTML); + } catch (e) { /* ignore quota */ } + } + + function renderProjectReport(pageTotalsMap, force) { + var body = getReportBodyEl(); + if (!body) return; + if (state.reportEdited && !force) return; + body.innerHTML = buildProjectReportHtml(); + saveReportToStorage(); + } + + function refreshProjectReport() { + state.reportEdited = false; + try { + localStorage.removeItem('feep-price-report-html'); + } catch (e) { /* ignore */ } + renderProjectReport(null, true); + showReportToast('گزارش از داده‌های فعلی به‌روزرسانی شد.'); + } + + function onReportEdited() { + state.reportEdited = true; + saveReportToStorage(); + } + + function showReportToast(message) { + var toast = document.getElementById('calc-report-toast'); + if (!toast) return; + toast.textContent = message; + toast.hidden = false; + clearTimeout(showReportToast._timer); + showReportToast._timer = setTimeout(function () { + toast.hidden = true; + }, 2500); + } + + function copyProjectReport() { + var body = getReportBodyEl(); + if (!body) return; + var text = buildProjectReportText(); + if (!text) return; + + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(text).then(function () { + showReportToast('متن گزارش در کلیپ‌بورد کپی شد.'); + }).catch(function () { + selectProjectReport(); + try { + document.execCommand('copy'); + showReportToast('متن گزارش در کلیپ‌بورد کپی شد.'); + } catch (e) { + showReportToast('متن را دستی انتخاب و کپی کنید (Ctrl+C).'); + } + }); + return; + } + selectProjectReport(); + document.execCommand('copy'); + showReportToast('متن گزارش در کلیپ‌بورد کپی شد.'); + } + + function selectProjectReport() { + var body = getReportBodyEl(); + if (!body) return; + body.focus(); + var range = document.createRange(); + range.selectNodeContents(body); + var sel = window.getSelection(); + sel.removeAllRanges(); + sel.addRange(range); + } + + function downloadProjectReport() { + var body = getReportBodyEl(); + if (!body || !body.innerHTML.trim()) return; + var docTitle = getProjectTitle() ? 'گزارش پروژه — ' + getProjectTitle() : 'گزارش برآورد پروژه'; + var htmlDoc = '' + + '' + escapeHtml(docTitle) + '' + + '' + + body.innerHTML + ''; + var blob = new Blob([htmlDoc], { type: 'text/html;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var link = document.createElement('a'); + link.href = url; + link.download = getReportDownloadBasename() + '.html'; + link.click(); + URL.revokeObjectURL(url); + showReportToast('فایل HTML گزارش دانلود شد.'); + } + + function downloadProjectReportText() { + var text = buildProjectReportText(); + if (!text) return; + var blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); + var url = URL.createObjectURL(blob); + var link = document.createElement('a'); + link.href = url; + link.download = getReportDownloadBasename() + '.txt'; + link.click(); + URL.revokeObjectURL(url); + showReportToast('فایل متنی گزارش دانلود شد.'); + } + + function getReportPrintCss() { + var styleEl = document.getElementById('report-print-css'); + return styleEl ? styleEl.textContent : ''; + } + + function buildReportPrintDocument() { + var body = getReportBodyEl(); + var docTitle = getProjectTitle() ? 'گزارش پروژه — ' + getProjectTitle() : 'گزارش برآورد پروژه'; + return '' + + '' + escapeHtml(docTitle) + '' + + '' + + '' + + '' + + '' + + '
' + body.innerHTML + '
' + + ''; + } + + function downloadProjectReportPdf() { + var source = getReportBodyEl(); + if (!source || !source.innerHTML.trim()) { + if (!getArchivedPages().length) { + showReportToast('گزارش خالی است — ابتدا صفحه‌ای آرشیو کنید.'); + return; + } + showReportToast('گزارش خالی است — ابتدا «به‌روزرسانی» را بزنید.'); + return; + } + + var btn = document.getElementById('calc-report-download-pdf'); + var iframe = document.createElement('iframe'); + iframe.setAttribute('aria-hidden', 'true'); + iframe.style.cssText = 'position:fixed;width:0;height:0;border:0;opacity:0;pointer-events:none;'; + document.body.appendChild(iframe); + + var cleanedUp = false; + var printWin = iframe.contentWindow; + var printDoc = printWin.document; + + function cleanup() { + if (cleanedUp) return; + cleanedUp = true; + printWin.removeEventListener('afterprint', cleanup); + if (iframe.parentNode) iframe.parentNode.removeChild(iframe); + if (btn) btn.disabled = false; + } + + printWin.addEventListener('afterprint', cleanup); + + if (btn) btn.disabled = true; + showReportToast('پنجره چاپ باز شد — «ذخیره به PDF» یا Microsoft Print to PDF را انتخاب کنید.'); + + printDoc.open(); + printDoc.write(buildReportPrintDocument()); + printDoc.close(); + + function triggerPrint() { + printWin.focus(); + printWin.print(); + window.setTimeout(cleanup, 3000); + } + + if (printDoc.fonts && printDoc.fonts.ready) { + printDoc.fonts.ready.then(triggerPrint).catch(triggerPrint); + } else { + window.setTimeout(triggerPrint, 400); + } + } + + function recalculate() { + if (!rowsEl) return; + + if (!isRendering) { + saveActivePageFromDom(); + } + + var projectBlockUnits = 0; + var projectAnimUnits = 0; + var pageTotalsMap = {}; + + state.pages.forEach(function (page) { + var totals = calcPageTotals(page); + pageTotalsMap[page.id] = totals; + projectBlockUnits += totals.blockUnits; + projectAnimUnits += totals.animUnits; + }); + + var revisionUnits = getRevisionUnits(); + var totalUnits = projectBlockUnits + projectAnimUnits + revisionUnits; + var blocksCost = totalUnits * UNIT_PRICE; + + var contentPct = getContentPercent(); + var responsivePct = parseFloat(document.getElementById('opt-responsive').value) || 0; + var darkPct = parseFloat(document.getElementById('opt-dark').value) || 0; + var bilingualPct = parseFloat(document.getElementById('opt-bilingual').value) || 0; + + var revisionCost = revisionUnits * UNIT_PRICE; + var contentExtra = blocksCost * contentPct; + var afterContent = blocksCost + contentExtra; + var responsiveExtra = afterContent * responsivePct; + var afterResponsive = afterContent + responsiveExtra; + var darkExtra = afterResponsive * darkPct; + var afterDark = afterResponsive + darkExtra; + var bilingualExtra = afterDark * bilingualPct; + var total = afterDark + bilingualExtra; + + if (emptyEl) { + emptyEl.style.display = rowsEl.children.length ? 'none' : 'block'; + } + + var activeTotals = pageTotalsMap[activePageId] || { totalUnits: 0, cost: 0, pageMultiplier: 1 }; + var activePage = getActivePage(); + var pageTotalLabel = fmt(activeTotals.cost) + ' تومان'; + if (activeTotals.pageMultiplier > 1) { + pageTotalLabel = '× ' + activeTotals.pageMultiplier.toLocaleString('fa-IR') + ' — ' + pageTotalLabel; + } + document.getElementById('sum-page-total').textContent = pageTotalLabel; + var pageMultHint = document.getElementById('sum-page-multiplier-hint'); + if (pageMultHint) { + if (activeTotals.pageMultiplier > 1 && activePage) { + pageMultHint.hidden = false; + pageMultHint.textContent = 'قالب «' + activePage.name + '» — ' + + fmtUnit(activeTotals.baseTotalUnits || 0) + ' Unit × ' + + activeTotals.pageMultiplier.toLocaleString('fa-IR') + ' = ' + + fmtUnit(activeTotals.totalUnits) + ' Unit'; + } else { + pageMultHint.hidden = true; + pageMultHint.textContent = ''; + } + } + document.getElementById('sum-units').textContent = fmtUnit(totalUnits); + document.getElementById('sum-blocks').textContent = fmt(blocksCost) + ' تومان'; + document.getElementById('sum-content').textContent = contentExtra ? fmt(contentExtra) + ' تومان' : '—'; + document.getElementById('sum-responsive').textContent = responsiveExtra ? fmt(responsiveExtra) + ' تومان' : '—'; + document.getElementById('sum-dark').textContent = darkExtra ? fmt(darkExtra) + ' تومان' : '—'; + document.getElementById('sum-bilingual').textContent = bilingualExtra ? fmt(bilingualExtra) + ' تومان' : '—'; + document.getElementById('sum-revision').textContent = revisionCost ? fmt(revisionCost) + ' تومان' : '—'; + document.getElementById('sum-total').textContent = fmt(total) + ' تومان'; + + renderPageTabs(pageTotalsMap); + renderPagesList(pageTotalsMap); + renderArchiveList(pageTotalsMap); + renderProjectReport(pageTotalsMap); + updateReuseButton(); + updatePageActions(); + } + + function updatePageActions() { + var page = getActivePage(); + var archiveBtn = document.getElementById('calc-page-archive'); + var unarchiveBtn = document.getElementById('calc-page-unarchive'); + var banner = document.getElementById('calc-archive-banner'); + var readonly = isPageReadOnly(page); + + if (calcMainEl) { + calcMainEl.classList.remove('calc-main--readonly'); + } + + var pageNameEl = document.getElementById('calc-page-name'); + if (pageNameEl) pageNameEl.readOnly = readonly; + + var animEl = document.getElementById('opt-animation'); + var animUnitsEl = document.getElementById('opt-anim-units'); + if (animEl) animEl.disabled = readonly; + if (animUnitsEl) animUnitsEl.disabled = readonly; + var multEl = document.getElementById('opt-page-multiplier'); + if (multEl) multEl.disabled = false; + + if (banner) { + banner.hidden = !readonly; + } + if (archiveBtn) { + archiveBtn.hidden = readonly; + archiveBtn.disabled = !page || !page.rows.length; + } + if (unarchiveBtn) { + unarchiveBtn.hidden = !readonly; + } + var deleteArchiveBtn = document.getElementById('calc-page-delete-archive'); + if (deleteArchiveBtn) { + deleteArchiveBtn.hidden = !readonly; + } + } + + function renderPageTabs(pageTotalsMap) { + if (!tabsEl) return; + var drafts = getDraftPages(); + if (!drafts.length) { + tabsEl.innerHTML = 'پیش‌نویسی نیست — از آرشیو مشاهده کنید'; + return; + } + tabsEl.innerHTML = drafts.map(function (page) { + var totals = pageTotalsMap[page.id] || { cost: 0 }; + var active = page.id === activePageId ? ' is-active' : ''; + var multBadge = totals.pageMultiplier > 1 + ? ' ×' + totals.pageMultiplier.toLocaleString('fa-IR') + '' + : ''; + return ''; + }).join(''); + + tabsEl.querySelectorAll('.calc-page-tab').forEach(function (btn) { + btn.addEventListener('click', function () { + switchPage(parseInt(btn.dataset.pageId, 10)); + }); + }); + } + + function renderPagesList(pageTotalsMap) { + if (!pagesListEl) return; + var drafts = getDraftPages(); + if (!drafts.length) { + pagesListEl.innerHTML = '
پیش‌نویسی فعال نیست
'; + return; + } + pagesListEl.innerHTML = drafts.map(function (page) { + var totals = pageTotalsMap[page.id] || { totalUnits: 0, cost: 0 }; + var active = page.id === activePageId && !page.archived ? ' is-active' : ''; + var multNote = totals.pageMultiplier > 1 ? ' ×' + totals.pageMultiplier.toLocaleString('fa-IR') : ''; + return '
' + + '' + page.name + multNote + ' (' + fmtUnit(totals.totalUnits) + 'u)' + + '' + fmt(totals.cost) + '' + + '
'; + }).join(''); + + pagesListEl.querySelectorAll('.calc-page-summary-item').forEach(function (item) { + item.addEventListener('click', function () { + switchPage(parseInt(item.dataset.pageId, 10)); + }); + }); + } + + function renderArchiveList(pageTotalsMap) { + if (!archiveListEl) return; + var archived = getArchivedPages(); + if (!archived.length) { + archiveListEl.innerHTML = '
هنوز صفحه‌ای آرشیو نشده
'; + return; + } + archiveListEl.innerHTML = archived.map(function (page) { + var totals = pageTotalsMap[page.id] || { totalUnits: 0, cost: 0 }; + var viewing = page.id === activePageId && page.archived ? ' is-viewing' : ''; + var archMult = totals.pageMultiplier > 1 + ? ' ×' + totals.pageMultiplier.toLocaleString('fa-IR') + '' + : ''; + return '
' + + '' + page.name + archMult + 'آرشیو (' + fmtUnit(totals.totalUnits) + 'u)' + + '' + + '' + fmt(totals.cost) + '' + + '' + + '' + + '
'; + }).join(''); + + archiveListEl.querySelectorAll('.calc-archive-item').forEach(function (item) { + item.addEventListener('click', function (e) { + if (e.target.closest('.calc-archive-delete')) return; + switchPage(parseInt(item.dataset.pageId, 10)); + }); + }); + + archiveListEl.querySelectorAll('.calc-archive-delete').forEach(function (btn) { + btn.addEventListener('click', function (e) { + e.stopPropagation(); + deleteArchivedPageById(parseInt(btn.dataset.pageId, 10)); + }); + }); + } + + function updateReuseButton() { + var group = document.getElementById('calc-reuse-group'); + var select = document.getElementById('calc-reuse-select'); + if (!group || !select || !activePageId) return; + var page = getActivePage(); + if (!page) { + group.hidden = true; + return; + } + var registry = getReuseRegistryForPage(page); + if (!registry.length) { + group.hidden = true; + return; + } + group.hidden = false; + select.innerHTML = registry.map(function (r) { + return ''; + }).join(''); + } + + function switchPage(pageId) { + if (pageId === activePageId) return; + saveActivePageFromDom(); + activePageId = pageId; + renderActivePage(); + recalculate(); + } + + function renderActivePage() { + var page = getActivePage(); + if (!page) return; + + isRendering = true; + + document.getElementById('calc-page-name').value = page.name; + document.getElementById('opt-animation').value = page.animation; + document.getElementById('opt-anim-units').value = page.animUnits; + document.getElementById('anim-extra-wrap').classList.toggle('hidden', page.animation !== 'complex'); + var multEl = document.getElementById('opt-page-multiplier'); + if (multEl) multEl.value = getPageMultiplier(page); + + document.getElementById('calc-page-delete').disabled = state.pages.length <= 1 || page.archived; + + sanitizePageLayoutRows(page); + rowsEl.innerHTML = ''; + getRowsForPage(page).forEach(function (rowData) { + if (rowData.mode === 'reuse' && !rowData.sourcePageName) { + var srcPage = state.pages.find(function (p) { return p.id === rowData.sourcePageId; }); + rowData.sourcePageName = srcPage ? srcPage.name : 'صفحه قبل'; + } + createRow(rowData); + }); + + if (!getRowsForPage(page).length && emptyEl) { + emptyEl.style.display = 'block'; + } + + isRendering = false; + } + + function getSelectableBlocks(catKey, page) { + var blocks = Object.keys(CATALOG[catKey].blocks); + if (page && shouldExcludeLayoutForPage(page)) { + blocks = blocks.filter(function (b) { return !isLayoutBlock(b); }); + } + return blocks; + } + + function createRow(defaults) { + var isReuse = defaults && defaults.mode === 'reuse'; + var rowDefaults = defaults || { mode: 'new', cat: 'general', reuse: 'new_block', complexity: 'medium' }; + if (!rowDefaults.id) rowDefaults.id = nextRowId(); + + var cat = rowDefaults.cat || 'general'; + var block = rowDefaults.block || Object.keys(CATALOG[cat].blocks)[0]; + + var row = document.createElement('div'); + row.className = 'calc-row' + (isReuse ? ' is-reuse' : ''); + row.dataset.id = String(rowDefaults.id); + row.dataset.mode = isReuse ? 'reuse' : 'new'; + if (isReuse) { + row.dataset.sourcePageId = String(rowDefaults.sourcePageId); + row.dataset.sourceRowId = String(rowDefaults.sourceRowId); + if (rowDefaults.reuseMatchType) row.dataset.reuseMatchType = rowDefaults.reuseMatchType; + } + + var reuseOptions = buildOptions(REUSE, REUSE_LABELS); + + var badgeHtml = isReuse + ? '
↩ Reuse از ' + (rowDefaults.sourcePageName || 'صفحه قبل') + + (rowDefaults.reuseMatchType && rowDefaults.reuseMatchType !== 'exact' + ? ' (' + (rowDefaults.reuseMatchType === 'similar-type' ? 'بلاک مشابه' : rowDefaults.reuseMatchType === 'fingerprint' ? 'ساختار مشابه' : 'دسته مشابه') + ')' + : '') + + '
' + : ''; + + row.innerHTML = badgeHtml + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '۰' + + '
' + + ''; + + var selCat = row.querySelector('.sel-cat'); + var selBlock = row.querySelector('.sel-block'); + var selComplexity = row.querySelector('.sel-complexity'); + var selReuse = row.querySelector('.sel-reuse'); + var complexityField = row.querySelector('.calc-field-complexity'); + + selCat.value = cat; + + function populateBlocks() { + var c = CATALOG[selCat.value]; + var activePage = getActivePage(); + selBlock.innerHTML = getSelectableBlocks(selCat.value, activePage).map(function (b) { + return ''; + }).join(''); + if (rowDefaults.block && c.blocks[rowDefaults.block] && (!activePage || !shouldExcludeLayoutForPage(activePage) || !isLayoutBlock(rowDefaults.block))) { + selBlock.value = rowDefaults.block; + } else if (selBlock.options.length) { + selBlock.selectedIndex = 0; + } + toggleComplexity(); + } + + function toggleComplexity() { + var has = CATALOG[selCat.value].hasComplexity; + complexityField.style.opacity = has ? '1' : '0.4'; + selComplexity.disabled = !has; + } + + function updateUnits() { + var base = getBlockUnit(selCat.value, selBlock.value, selComplexity.value); + var reuseKey = selReuse.value; + var factor = REUSE[reuseKey] != null ? REUSE[reuseKey] : 1; + var units = base * factor; + var cost = units * UNIT_PRICE; + row.querySelector('.unit-val').textContent = fmtUnit(units) + ' Unit'; + row.querySelector('.unit-cost').textContent = fmt(cost) + ' تومان'; + recalculate(); + } + + selCat.addEventListener('change', function () { populateBlocks(); updateUnits(); }); + selBlock.addEventListener('change', updateUnits); + selComplexity.addEventListener('change', updateUnits); + selReuse.addEventListener('change', function () { + if (row.dataset.mode === 'reuse' && selReuse.value === 'new_block') { + row.dataset.mode = 'new'; + row.classList.remove('is-reuse'); + delete row.dataset.sourcePageId; + delete row.dataset.sourceRowId; + delete row.dataset.reuseMatchType; + var badge = row.querySelector('.reuse-badge'); + if (badge) badge.remove(); + } + updateUnits(); + }); + row.querySelector('.calc-remove').addEventListener('click', function () { + row.remove(); + recalculate(); + }); + + populateBlocks(); + if (rowDefaults.complexity) selComplexity.value = rowDefaults.complexity; + if (rowDefaults.reuse) selReuse.value = rowDefaults.reuse; + updateUnits(); + + rowsEl.appendChild(row); + if (emptyEl) emptyEl.style.display = 'none'; + + return row; + } + + function addReuseRow() { + var page = getActivePage(); + if (!page) return; + + var registry = getReuseRegistryForPage(page); + if (!registry.length) return; + + var select = document.getElementById('calc-reuse-select'); + var key = select ? select.value : ''; + var choice = registry.find(function (r) { + return (r.sourcePageId + ':' + r.sourceRowId) === key; + }) || registry[0]; + + if (!page) return; + + var newRow = { + id: nextRowId(), + mode: 'reuse', + sourcePageId: choice.sourcePageId, + sourceRowId: choice.sourceRowId, + sourcePageName: choice.pageName, + cat: choice.cat, + block: choice.block, + complexity: choice.complexity, + reuse: 'no_change' + }; + + createRow(newRow); + recalculate(); + } + + function addPage() { + saveActivePageFromDom(); + var page = createPageData(getNextDraftPageName(), []); + page.projectPageNumber = getArchivedPages().length + 1; + state.pages.push(page); + activePageId = page.id; + renderActivePage(); + recalculate(); + } + + function archivePage() { + var page = getActivePage(); + if (!page || page.archived) return; + + saveActivePageFromDom(); + if (!page.rows.length) return; + + if (!page.projectPageNumber) { + page.projectPageNumber = getArchivedPages().length + 1; + } + sanitizePageLayoutRows(page); + page.archived = true; + if (!page.archivedAt) { + page.archivedAt = new Date().toISOString(); + } + + var drafts = getDraftPages(); + if (drafts.length) { + activePageId = drafts[0].id; + } else { + var newPage = createPageData(getNextDraftPageName(), []); + newPage.projectPageNumber = getArchivedPages().length + 1; + state.pages.push(newPage); + activePageId = newPage.id; + } + + renderActivePage(); + recalculate(); + } + + function unarchivePage() { + var page = getActivePage(); + if (!page || !page.archived) return; + + page.archived = false; + renderActivePage(); + recalculate(); + } + + function removeReuseReferencesToPage(pageId) { + state.pages.forEach(function (p) { + p.rows = p.rows.filter(function (row) { + return !(row.mode === 'reuse' && row.sourcePageId === pageId); + }); + }); + } + + function switchAfterPageRemoved(removedIdx) { + var drafts = getDraftPages(); + if (drafts.length) { + activePageId = drafts[0].id; + return; + } + if (state.pages.length) { + var next = state.pages[Math.min(removedIdx, state.pages.length - 1)]; + activePageId = next.id; + return; + } + var newPage = createPageData(getNextDraftPageName(), []); + state.pages.push(newPage); + activePageId = newPage.id; + } + + function deleteArchivedPageById(pageId) { + var page = state.pages.find(function (p) { return p.id === pageId; }); + if (!page || !page.archived) return; + + var label = page.name || 'این صفحه'; + if (!window.confirm('صفحه آرشیوشده «' + label + '» حذف شود؟\nبلاک‌های Reuse مرتبط با این صفحه هم حذف می‌شوند.')) { + return; + } + + removeReuseReferencesToPage(pageId); + var idx = getPageIndex(pageId); + state.pages.splice(idx, 1); + + if (activePageId === pageId) { + switchAfterPageRemoved(idx); + renderActivePage(); + } + + recalculate(); + } + + function deleteArchivedPage() { + var page = getActivePage(); + if (!page || !page.archived) return; + deleteArchivedPageById(page.id); + } + + function deletePage() { + var page = getActivePage(); + if (!page || page.archived) return; + if (state.pages.length <= 1) return; + saveActivePageFromDom(); + var idx = getPageIndex(activePageId); + state.pages.splice(idx, 1); + var next = state.pages[Math.min(idx, state.pages.length - 1)]; + activePageId = next.id; + renderActivePage(); + recalculate(); + } + + var VALID_COMPLEXITY = { simple: true, medium: true, complex: true }; + + function blockToSlug(name) { + return name.toLowerCase().replace(/\s+/g, '-'); + } + + function normalizeToken(str) { + return (str || '').trim().toLowerCase().replace(/[_-]+/g, ' ').replace(/\s+/g, ' '); + } + + function getCatalogIndex() { + var index = []; + Object.keys(CATALOG).forEach(function (catKey) { + Object.keys(CATALOG[catKey].blocks).forEach(function (blockName) { + index.push({ + cat: catKey, + block: blockName, + normalized: normalizeToken(blockName), + slug: blockToSlug(blockName), + compact: normalizeToken(blockName).replace(/\s/g, '') + }); + }); + }); + return index; + } + + var catalogIndex = getCatalogIndex(); + + function resolveCatalogBlock(blockName, catHint) { + if (!blockName) return null; + var normalized = normalizeToken(blockName); + var compact = normalized.replace(/\s/g, ''); + var slug = blockName.trim().toLowerCase().replace(/\s+/g, '-'); + + if (catHint && CATALOG[catHint] && CATALOG[catHint].blocks[blockName]) { + return { cat: catHint, block: blockName }; + } + + for (var i = 0; i < catalogIndex.length; i++) { + var item = catalogIndex[i]; + if (item.block === blockName) return { cat: item.cat, block: item.block }; + if (item.normalized === normalized || item.compact === compact || item.slug === slug) { + return { cat: item.cat, block: item.block }; + } + } + return null; + } + + function normalizeComplexity(value) { + var v = (value || '').trim().toLowerCase(); + return VALID_COMPLEXITY[v] ? v : 'medium'; + } + + function makeImportRow(cat, block, complexity, extra) { + var row = { + mode: 'new', + cat: cat, + block: block, + complexity: normalizeComplexity(complexity), + reuse: 'new_block' + }; + if (extra) { + Object.keys(extra).forEach(function (k) { + row[k] = extra[k]; + }); + } + return row; + } + + var BLOCK_CLASSIFIERS = [ + { cat: 'general', block: 'Navbar', weight: 10, test: function (el, ctx) { + if (el.matches('header, [role="banner"]')) return true; + if (el.matches('nav') && !el.closest('footer')) return true; + return /nav(bar|igation)?/i.test(ctx.hay); + }}, + { cat: 'general', block: 'Footer', weight: 10, test: function (el, ctx) { + return el.matches('footer, [role="contentinfo"]') || /footer/i.test(ctx.hay); + }}, + { cat: 'general', block: 'Hero', weight: 9, test: function (el, ctx) { + return /hero|jumbotron|masthead|page-header/i.test(ctx.hay) && !/feature/i.test(ctx.hay); + }}, + { cat: 'general', block: 'Pricing', weight: 9, test: function (el, ctx) { + return /pricing|price-table|plans?/i.test(ctx.hay); + }}, + { cat: 'general', block: 'FAQ', weight: 9, test: function (el, ctx) { + return /faq|frequently-asked|accordion.*question/i.test(ctx.hay) || (el.querySelector('details, [data-slot="accordion"]') && /faq|question/i.test(ctx.hay)); + }}, + { cat: 'general', block: 'Testimonial', weight: 8, test: function (el, ctx) { + return /testimonial|review|quote|customer-story/i.test(ctx.hay); + }}, + { cat: 'general', block: 'Gallery', weight: 8, test: function (el, ctx) { + return /gallery|portfolio|lightbox/i.test(ctx.hay) || (el.querySelectorAll('img').length >= 4 && /gallery|portfolio/i.test(ctx.hay)); + }}, + { cat: 'general', block: 'Feature Grid', weight: 8, test: function (el, ctx) { + return /feature|benefit|capabilit/i.test(ctx.hay) && (el.querySelectorAll('[class*="card"], [class*="grid"] > *').length >= 3 || /grid/i.test(ctx.hay)); + }}, + { cat: 'general', block: 'Card Section', weight: 7, test: function (el, ctx) { + return el.querySelectorAll('[class*="card"]').length >= 2 && !/pricing|testimonial/i.test(ctx.hay); + }}, + { cat: 'general', block: 'CTA', weight: 8, test: function (el, ctx) { + return /(^|\s)cta(\s|$)|call-to-action|get-started/i.test(ctx.hay); + }}, + { cat: 'general', block: 'Banner', weight: 7, test: function (el, ctx) { + return /banner|promo|announcement/i.test(ctx.hay) && !/hero/i.test(ctx.hay); + }}, + { cat: 'general', block: 'Timeline', weight: 7, test: function (el, ctx) { + return /timeline|roadmap|history/i.test(ctx.hay); + }}, + { cat: 'general', block: 'Sidebar', weight: 8, test: function (el, ctx) { + return el.matches('aside') || /sidebar|side-nav/i.test(ctx.hay); + }}, + { cat: 'general', block: 'Blog List', weight: 8, test: function (el, ctx) { + return /blog|article-list|posts?/i.test(ctx.hay) && el.querySelectorAll('article, [class*="post"]').length >= 2; + }}, + { cat: 'general', block: 'Contact Section', weight: 8, test: function (el, ctx) { + return /contact|get-in-touch/i.test(ctx.hay) || (el.querySelector('form') && /contact/i.test(ctx.hay)); + }}, + { cat: 'dashboard', block: 'Data Table', weight: 9, test: function (el, ctx) { + return el.querySelector('table') && /table|data-grid|datatable/i.test(ctx.hay); + }}, + { cat: 'dashboard', block: 'Stat Card', weight: 8, test: function (el, ctx) { + return /stat|metric|kpi|counter/i.test(ctx.hay); + }}, + { cat: 'dashboard', block: 'Chart', weight: 8, test: function (el, ctx) { + return /chart|graph|analytics/i.test(ctx.hay) || el.querySelector('canvas, svg[class*="chart"]'); + }}, + { cat: 'dashboard', block: 'Filter Bar', weight: 7, test: function (el, ctx) { + return /filter|facets?/i.test(ctx.hay); + }}, + { cat: 'dashboard', block: 'Widget', weight: 6, test: function (el, ctx) { + return /widget|panel/i.test(ctx.hay); + }}, + { cat: 'form', block: 'Form Section', weight: 9, test: function (el, ctx) { + return el.querySelector('form') && !/search|login|sign-?in/i.test(ctx.hay); + }}, + { cat: 'form', block: 'Multi Step', weight: 9, test: function (el, ctx) { + return /multi-?step|wizard|stepper/i.test(ctx.hay); + }}, + { cat: 'form', block: 'File Upload', weight: 8, test: function (el, ctx) { + return el.querySelector('input[type="file"]') || /upload|dropzone/i.test(ctx.hay); + }}, + { cat: 'interactive', block: 'Modal', weight: 9, test: function (el, ctx) { + return el.matches('[role="dialog"], [data-slot="dialog"], .modal') || /modal|dialog/i.test(ctx.hay); + }}, + { cat: 'interactive', block: 'Drawer', weight: 8, test: function (el, ctx) { + return /drawer|sheet|offcanvas/i.test(ctx.hay); + }} + ]; + + var STRUCTURAL_HEURISTICS = [ + { selector: 'header, [role="banner"]', cat: 'general', block: 'Navbar' }, + { selector: 'nav:not(header nav):not(footer nav)', cat: 'general', block: 'Navbar' }, + { selector: 'footer, [role="contentinfo"]', cat: 'general', block: 'Footer' }, + { selector: '[class*="hero" i], [id*="hero" i], [data-slot*="hero" i]', cat: 'general', block: 'Hero' }, + { selector: '[class*="pricing" i], [id*="pricing" i]', cat: 'general', block: 'Pricing' }, + { selector: '[class*="faq" i], [id*="faq" i]', cat: 'general', block: 'FAQ' }, + { selector: '[class*="testimonial" i], [id*="testimonial" i]', cat: 'general', block: 'Testimonial' }, + { selector: '[class*="gallery" i], [id*="gallery" i]', cat: 'general', block: 'Gallery' }, + { selector: '[class*="sidebar" i], aside', cat: 'general', block: 'Sidebar' }, + { selector: '[class*="cta" i], [id*="cta" i]', cat: 'general', block: 'CTA' }, + { selector: '[class*="banner" i], [id*="banner" i]', cat: 'general', block: 'Banner' }, + { selector: '[class*="timeline" i], [id*="timeline" i]', cat: 'general', block: 'Timeline' }, + { selector: '[class*="feature-grid" i], [class*="features" i][class*="grid" i]', cat: 'general', block: 'Feature Grid' }, + { selector: '[class*="contact" i][class*="section" i], [id*="contact" i]', cat: 'general', block: 'Contact Section' }, + { selector: '[class*="data-table" i], table[data-slot], .data-table', cat: 'dashboard', block: 'Data Table' }, + { selector: '[class*="stat-card" i], [class*="stats" i]', cat: 'dashboard', block: 'Stat Card' }, + { selector: '[class*="chart" i]:not(canvas)', cat: 'dashboard', block: 'Chart' }, + { selector: '[role="dialog"], [data-slot="dialog"], .modal', cat: 'interactive', block: 'Modal' }, + { selector: '[data-slot="drawer"], .drawer', cat: 'interactive', block: 'Drawer' }, + { selector: '[data-slot="dropdown-menu"], .dropdown-menu', cat: 'interactive', block: 'Dropdown' } + ]; + + function getElementHaystack(el) { + var parts = []; + if (el.id) parts.push(el.id); + if (el.className && typeof el.className === 'string') parts.push(el.className); + if (el.getAttribute('data-slot')) parts.push(el.getAttribute('data-slot')); + if (el.getAttribute('data-section')) parts.push(el.getAttribute('data-section')); + if (el.getAttribute('aria-label')) parts.push(el.getAttribute('aria-label')); + var heading = el.querySelector('h1, h2, h3'); + if (heading) parts.push(heading.textContent || ''); + return parts.join(' ').toLowerCase(); + } + + function classifyElement(el) { + var ctx = { hay: getElementHaystack(el) }; + var best = null; + var bestWeight = 0; + + BLOCK_CLASSIFIERS.forEach(function (rule) { + try { + if (rule.test(el, ctx) && rule.weight > bestWeight) { + best = rule; + bestWeight = rule.weight; + } + } catch (e) { /* ignore */ } + }); + + if (best) { + return { cat: best.cat, block: best.block, complexity: inferComplexity(el, best.block) }; + } + return null; + } + + function inferComplexity(el, blockName) { + var childCount = el.querySelectorAll('*').length; + var inputs = el.querySelectorAll('input, select, textarea, button').length; + var cards = el.querySelectorAll('[class*="card"], li, article').length; + + if (blockName === 'Data Table' || blockName === 'Multi Step' || blockName === 'Kanban') { + if (childCount > 80 || inputs > 8) return 'complex'; + if (childCount > 35 || inputs > 3) return 'medium'; + return 'simple'; + } + if (blockName === 'Hero' || blockName === 'Pricing' || blockName === 'Gallery') { + if (childCount > 60 || cards > 6) return 'complex'; + if (childCount > 25 || cards > 3) return 'medium'; + return 'simple'; + } + if (childCount > 50 || inputs > 5 || cards > 5) return 'complex'; + if (childCount > 20 || inputs > 2 || cards > 2) return 'medium'; + return 'simple'; + } + + function findMainRoot(doc) { + return doc.querySelector('main, [role="main"], #main, .main, #content, .content, .page, [class*="page-content"]') || doc.body; + } + + function isHiddenElement(el) { + if (!el || el.nodeType !== 1) return true; + if (el.closest('[hidden], [aria-hidden="true"], template, script, style, noscript')) return true; + var tag = el.tagName.toLowerCase(); + if (tag === 'script' || tag === 'style' || tag === 'noscript' || tag === 'link' || tag === 'meta') return true; + if (el.matches('[data-elementor-type="popup"], [data-elementor-type="floating-buttons"], .elementor-location-popup, .elementor-location-floating_buttons')) return true; + return false; + } + + function getSectionText(el) { + return (el.textContent || '').replace(/\s+/g, ' ').trim(); + } + + function hasMeaningfulContent(el) { + var text = getSectionText(el); + if (text.length > 12) return true; + return !!el.querySelector('img, svg, video, form, table, input, button, a[href], canvas'); + } + + function getHiddenBreakpoints(el) { + var cls = (el.className && typeof el.className === 'string') ? el.className : ''; + var matches = cls.match(/elementor-hidden-([a-z_]+)/gi) || []; + return matches.map(function (m) { return m.toLowerCase(); }); + } + + function areResponsiveAlternates(a, b) { + var aHidden = getHiddenBreakpoints(a); + var bHidden = getHiddenBreakpoints(b); + if (!aHidden.length || !bHidden.length) return false; + if (aHidden.join('|') === bHidden.join('|')) return false; + var aWidgets = getWidgetFingerprint(a); + var bWidgets = getWidgetFingerprint(b); + if (aWidgets && aWidgets === bWidgets) return true; + var aHeading = (a.querySelector('h1, h2, h3') || {}).textContent || ''; + var bHeading = (b.querySelector('h1, h2, h3') || {}).textContent || ''; + if (aHeading && bHeading && aHeading.trim() === bHeading.trim()) return true; + return false; + } + + function getWidgetFingerprint(el) { + var types = []; + el.querySelectorAll('[data-widget_type]').forEach(function (w) { + var t = w.getAttribute('data-widget_type'); + if (!t) return; + if (t === 'image.default' || t === 'spacer.default') return; + types.push(t); + }); + types.sort(); + return types.join('|'); + } + + function isHeadingOnlySection(el) { + if (!el || el.matches('header, footer, nav')) return false; + var widgets = el.querySelectorAll('[data-widget_type]'); + if (widgets.length > 1) return false; + var text = getSectionText(el); + if (text.length > 50) return false; + return !!el.querySelector('h1, h2, h3, h4'); + } + + function filterHeadingOnlySections(sections) { + return sections.filter(function (section) { + if (section.role === 'layout-header' || section.role === 'layout-footer' || section.role === 'explicit') { + return true; + } + return !isHeadingOnlySection(section.el); + }).map(function (section, idx) { + return Object.assign({}, section, { index: idx + 1 }); + }); + } + + function finalizeSections(sections) { + return filterHeadingOnlySections(mergeResponsiveSections(sections)); + } + + function mergeResponsiveSections(sections) { + if (!sections.length) return []; + var merged = []; + var skip = Object.create(null); + + sections.forEach(function (section, i) { + if (skip[i]) return; + var group = [section]; + for (var j = i + 1; j < sections.length; j++) { + if (skip[j]) continue; + if (areResponsiveAlternates(section.el, sections[j].el)) { + group.push(sections[j]); + skip[j] = true; + } + } + merged.push({ + el: section.el, + source: section.source, + role: section.role, + index: merged.length + 1, + mergedCount: group.length + }); + }); + + return merged; + } + + function discoverExplicitSections(doc) { + var sections = []; + function add(el, source) { + if (!el || isHiddenElement(el)) return; + sections.push({ el: el, source: source, role: 'explicit' }); + } + + doc.querySelectorAll('[data-feep-block]').forEach(function (el) { add(el, 'data-feep-block'); }); + doc.querySelectorAll('[data-block]').forEach(function (el) { add(el, 'data-block'); }); + doc.querySelectorAll('[data-block-name]').forEach(function (el) { add(el, 'data-block-name'); }); + return sections; + } + + function discoverElementorSections(doc) { + var sections = []; + var pageRoot = doc.querySelector('[data-elementor-type="wp-page"].elementor, .elementor-page.elementor'); + + if (pageRoot) { + Array.from(pageRoot.children).forEach(function (child) { + if (!child.matches('.e-con.e-parent')) return; + if (isHiddenElement(child) || !hasMeaningfulContent(child)) return; + sections.push({ el: child, source: 'elementor-section', role: 'content' }); + }); + } + + var header = doc.querySelector('.elementor-location-header, header.elementor[data-elementor-type="header"]'); + if (header && hasMeaningfulContent(header)) { + sections.unshift({ el: header, source: 'elementor-header', role: 'layout-header' }); + } + + var footer = doc.querySelector('.elementor-location-footer, footer.elementor[data-elementor-type="footer"]'); + if (footer && hasMeaningfulContent(footer)) { + sections.push({ el: footer, source: 'elementor-footer', role: 'layout-footer' }); + } + + return sections; + } + + function discoverSemanticSections(doc) { + var sections = []; + var bodyHeader = doc.querySelector('body > header, body > [role="banner"]'); + var bodyFooter = doc.querySelector('body > footer, body > [role="contentinfo"]'); + var bodyNav = doc.querySelector('body > nav'); + var root = findMainRoot(doc); + + if (bodyHeader && hasMeaningfulContent(bodyHeader)) { + sections.push({ el: bodyHeader, source: 'semantic-header', role: 'layout-header' }); + } else if (bodyNav && hasMeaningfulContent(bodyNav)) { + sections.push({ el: bodyNav, source: 'semantic-nav', role: 'layout-header' }); + } + + var mainChildren = []; + if (root) { + Array.from(root.children).forEach(function (child) { + if (!child.matches('section, article, div, header, footer, aside, nav')) return; + if (isHiddenElement(child) || !hasMeaningfulContent(child)) return; + mainChildren.push(child); + }); + } + + if (!mainChildren.length) { + doc.querySelectorAll('main section, main article, body > section, body > article').forEach(function (el) { + if (isHiddenElement(el) || !hasMeaningfulContent(el)) return; + mainChildren.push(el); + }); + } + + var unique = mainChildren.filter(function (el, idx, arr) { + for (var i = 0; i < arr.length; i++) { + if (arr[i] !== el && arr[i].contains(el)) return false; + } + return true; + }); + + unique.forEach(function (el) { + sections.push({ el: el, source: 'semantic-section', role: 'content' }); + }); + + if (bodyFooter && hasMeaningfulContent(bodyFooter)) { + sections.push({ el: bodyFooter, source: 'semantic-footer', role: 'layout-footer' }); + } + + return sections; + } + + function discoverPageSections(doc) { + var explicit = discoverExplicitSections(doc); + if (explicit.length) return finalizeSections(explicit); + + var elementor = discoverElementorSections(doc); + if (elementor.length >= 2) return finalizeSections(elementor); + + var semantic = discoverSemanticSections(doc); + if (semantic.length) return finalizeSections(semantic); + + return []; + } + + function classifyByRole(section) { + if (section.role === 'layout-header') { + return { cat: 'general', block: 'Navbar', complexity: inferComplexity(section.el, 'Navbar') }; + } + if (section.role === 'layout-footer') { + return { cat: 'general', block: 'Footer', complexity: inferComplexity(section.el, 'Footer') }; + } + return null; + } + + function classifyByWidgets(el) { + var widgetTypes = []; + el.querySelectorAll('[data-widget_type]').forEach(function (w) { + widgetTypes.push(w.getAttribute('data-widget_type') || ''); + }); + var joined = widgetTypes.join(' ').toLowerCase(); + + if (/nadernavigation|nav-menu|mega-menu/i.test(joined) || el.querySelector('nav, .nader-navigation, #nav')) { + return { cat: 'general', block: 'Navbar', complexity: inferComplexity(el, 'Navbar') }; + } + if (/tabswithimage|nested-tabs|tabs\./i.test(joined)) { + return { cat: 'dashboard', block: 'Tabs', complexity: inferComplexity(el, 'Tabs') }; + } + if (/postsslider|posts-slider|blog|loop-grid|archive-posts/i.test(joined)) { + return { cat: 'general', block: 'Blog List', complexity: inferComplexity(el, 'Blog List') }; + } + if (/infinitecss|logo-slider|carousel|swiper|image-carousel/i.test(joined)) { + return { cat: 'general', block: 'Testimonial', complexity: inferComplexity(el, 'Testimonial') }; + } + if (/accordion|faq/i.test(joined)) { + return { cat: 'general', block: 'FAQ', complexity: inferComplexity(el, 'FAQ') }; + } + if (/form\./i.test(joined) || (el.querySelector('form') && !el.querySelector('header, nav'))) { + return { cat: 'form', block: 'Form Section', complexity: inferComplexity(el, 'Form Section') }; + } + if (/price-table|pricing/i.test(joined)) { + return { cat: 'general', block: 'Pricing', complexity: inferComplexity(el, 'Pricing') }; + } + if (/animated-headline|slides|video/i.test(joined) && el.querySelector('h1')) { + return { cat: 'general', block: 'Hero', complexity: inferComplexity(el, 'Hero') }; + } + if (/animated-headline/i.test(joined)) { + return { cat: 'general', block: 'Banner', complexity: inferComplexity(el, 'Banner') }; + } + return null; + } + + function classifySection(section) { + var roleMatch = classifyByRole(section); + if (roleMatch) return Object.assign({ label: roleMatch.block }, roleMatch); + + var el = section.el; + var widgetMatch = classifyByWidgets(el); + if (widgetMatch) return Object.assign({ label: widgetMatch.block }, widgetMatch); + + var classified = classifyElement(el); + if (classified) return Object.assign({ label: classified.block }, classified); + + var heading = el.querySelector('h1, h2, h3'); + var headingText = heading ? heading.textContent.trim() : ''; + return { + cat: 'general', + block: 'Card Section', + complexity: inferComplexity(el, 'Card Section'), + label: headingText ? 'Card Section — ' + headingText.slice(0, 30) : 'Card Section' + }; + } + + function parseExplicitMarkers(doc) { + var rows = []; + + doc.querySelectorAll('script[type="application/json"][data-feep-blocks], script#feep-blocks').forEach(function (el) { + try { + var data = JSON.parse(el.textContent || '[]'); + var list = Array.isArray(data) ? data : (data.blocks || []); + list.forEach(function (item) { + if (!item || !item.block) return; + var match = resolveCatalogBlock(item.block, item.cat || item.category); + if (match) rows.push(makeImportRow(match.cat, match.block, item.complexity)); + }); + } catch (e) { /* invalid json */ } + }); + + var commentPattern = /feep:block(?::([^\s>]+))?(?::([^\s>]+))?(?::([^\s>]+))?/gi; + var walker = doc.createTreeWalker(doc, NodeFilter.SHOW_COMMENT, null); + var commentNode; + while ((commentNode = walker.nextNode())) { + var text = commentNode.nodeValue || ''; + var match; + commentPattern.lastIndex = 0; + while ((match = commentPattern.exec(text))) { + var c1 = match[1]; + var c2 = match[2]; + var c3 = match[3]; + var catHint = null; + var blockName = c1; + var complexity = 'medium'; + if (c2 && CATALOG[c1] && CATALOG[c1].blocks[c2]) { + catHint = c1; + blockName = c2; + complexity = c3 || 'medium'; + } else if (c2) { + complexity = c2; + } + var resolved = resolveCatalogBlock(blockName, catHint); + if (resolved) rows.push(makeImportRow(resolved.cat, resolved.block, complexity)); + } + } + + return rows; + } + + function analyzePageFromHtml(html) { + var doc = new DOMParser().parseFromString(html, 'text/html'); + var markerRows = parseExplicitMarkers(doc); + + if (markerRows.length) { + return { + rows: markerRows, + pageTitle: getPageTitle(doc), + meta: { + phase1Count: markerRows.length, + phase2Count: markerRows.length, + mergedResponsive: 0, + source: 'markers' + } + }; + } + + var rawSections = discoverExplicitSections(doc); + if (!rawSections.length) { + rawSections = discoverElementorSections(doc); + } + if (rawSections.length < 2) { + rawSections = discoverSemanticSections(doc); + } + + var phase1Count = rawSections.length; + var sections = finalizeSections(rawSections); + + if (!sections.length) { + return { + rows: [], + pageTitle: getPageTitle(doc), + meta: { phase1Count: 0, phase2Count: 0, mergedResponsive: 0, source: 'none' } + }; + } + + var analyzed = sections.map(function (section) { + var explicitBlock = section.role === 'explicit' ? section.el.getAttribute('data-feep-block') || section.el.getAttribute('data-block') || section.el.getAttribute('data-block-name') : null; + var explicitCat = section.el.getAttribute('data-feep-category') || section.el.getAttribute('data-feep-cat') || section.el.getAttribute('data-block-category'); + var explicitComplexity = section.el.getAttribute('data-feep-complexity') || section.el.getAttribute('data-block-complexity'); + + if (explicitBlock) { + var resolved = resolveCatalogBlock(explicitBlock, explicitCat); + if (resolved) { + return { + index: section.index, + cat: resolved.cat, + block: resolved.block, + complexity: normalizeComplexity(explicitComplexity), + label: resolved.block, + fingerprint: section.el ? getWidgetFingerprint(section.el) : '', + source: section.source + }; + } + } + + var classified = classifySection(section); + return { + index: section.index, + cat: classified.cat, + block: classified.block, + complexity: classified.complexity, + label: classified.label, + fingerprint: section.el ? getWidgetFingerprint(section.el) : '', + source: section.source + }; + }); + + var rows = analyzed.map(function (item) { + return makeImportRow(item.cat, item.block, item.complexity, { + label: item.label || item.block, + fingerprint: item.fingerprint || '' + }); + }); + + var mergedResponsive = Math.max(0, phase1Count - sections.length); + + return { + rows: rows, + analyzed: analyzed, + pageTitle: getPageTitle(doc), + meta: { + phase1Count: phase1Count, + phase2Count: analyzed.length, + mergedResponsive: mergedResponsive, + source: sections[0] ? sections[0].source : 'none' + } + }; + } + + function getPageTitle(doc) { + var pageName = doc.querySelector('[data-feep-page-name]'); + if (pageName) return pageName.getAttribute('data-feep-page-name'); + return (doc.querySelector('title') && doc.querySelector('title').textContent || '').trim(); + } + + function parseBlocksFromHtml(html) { + var result = analyzePageFromHtml(html); + return { + rows: result.rows, + pageTitle: result.pageTitle, + meta: result.meta, + analyzed: result.analyzed + }; + } + + function fetchPageHtml(url) { + var localApi = '/api/fetch-page?url=' + encodeURIComponent(url); + return fetch(localApi, { method: 'GET', credentials: 'same-origin' }) + .then(function (res) { + if (res.ok) return res.text(); + throw new Error('local-api'); + }) + .catch(function () { + return fetch(url, { method: 'GET', credentials: 'omit' }) + .then(function (res) { + if (!res.ok) throw new Error('HTTP ' + res.status); + return res.text(); + }); + }) + .catch(function () { + var proxyUrl = 'https://api.allorigins.win/raw?url=' + encodeURIComponent(url); + return fetch(proxyUrl, { method: 'GET' }).then(function (res) { + if (!res.ok) throw new Error('proxy'); + return res.text(); + }); + }); + } + + function setImportStatus(type, message) { + var el = document.getElementById('calc-import-status'); + if (!el) return; + el.hidden = false; + el.className = 'calc-import-status is-' + type; + el.textContent = message; + } + + function clearImportStatus() { + var el = document.getElementById('calc-import-status'); + if (!el) return; + el.hidden = true; + el.className = 'calc-import-status'; + el.textContent = ''; + } + + function applyImportedRows(rows, pageTitle, options) { + var page = getActivePage(); + if (!page || page.archived) { + setImportStatus('error', 'صفحه فعال قابل ویرایش نیست — یک پیش‌نویس انتخاب کنید.'); + return false; + } + + if (!rows.length) { + setImportStatus('error', 'هیچ بلاکی در این صفحه شناسایی نشد. از data-feep-block روی المان‌ها استفاده کنید یا ساختار HTML را واضح‌تر کنید.'); + return false; + } + + saveActivePageFromDom(); + + if (options.replace) { + page.rows = rows.map(function (r) { + return Object.assign({ id: nextRowId() }, r); + }); + } else { + rows.forEach(function (r) { + page.rows.push(Object.assign({ id: nextRowId() }, r)); + }); + } + + resolveIntraPageReuseReferences(page); + dedupeRowsWithinPage(page); + + if (options.useTitle && pageTitle) { + page.name = pageTitle.split(/[|\-–—]/)[0].trim() || pageTitle; + } + + if (options.sourceUrl) { + page.sourceUrl = options.sourceUrl; + } + + if (options.projectPageNumber) { + page.projectPageNumber = options.projectPageNumber; + } + + sanitizePageLayoutRows(page); + + renderActivePage(); + recalculate(); + return true; + } + + function autoArchiveActivePage() { + var page = getActivePage(); + if (!page || page.archived || !page.rows.length) return false; + + saveActivePageFromDom(); + if (!page.projectPageNumber) { + page.projectPageNumber = getArchivedPages().length + 1; + } + sanitizePageLayoutRows(page); + page.archived = true; + if (!page.archivedAt) { + page.archivedAt = new Date().toISOString(); + } + + var newPage = createPageData(getNextDraftPageName(), []); + newPage.projectPageNumber = getArchivedPages().length + 1; + state.pages.push(newPage); + activePageId = newPage.id; + renderActivePage(); + recalculate(); + return true; + } + + function buildImportStatusMessage(pageNum, matched, meta, archived) { + var parts = []; + if (meta) { + parts.push('فاز ۱: ' + meta.phase1Count + ' بلاک شمارش شد'); + if (meta.mergedResponsive) { + parts.push(meta.mergedResponsive + ' نسخه ریسپانسیو ادغام شد'); + } + parts.push('فاز ۲: ' + meta.phase2Count + ' بلاک شناسایی شد'); + } + parts.push('صفحه ' + pageNum + ' («' + matched.page.name + '»)'); + if (matched.stats.reuseCount) parts.push(matched.stats.reuseCount + ' Reuse'); + if (matched.stats.newCount) parts.push(matched.stats.newCount + ' جدید'); + if (matched.stats.skippedLayout) parts.push(matched.stats.skippedLayout + ' هدر/فوتر حذف شد'); + parts.push(archived ? 'فاز ۳: ثبت و آرشیو شد' : 'فاز ۳: به پیش‌نویس اضافه شد'); + return parts.join(' — '); + } + + function importFromUrl() { + var urlInput = document.getElementById('calc-import-url'); + var btn = document.getElementById('calc-import-btn'); + if (!urlInput || !btn) return; + + var rawUrl = urlInput.value.trim(); + if (!rawUrl) { + setImportStatus('error', 'لطفاً آدرس URL را وارد کنید.'); + return; + } + + var url; + try { + url = new URL(rawUrl).href; + } catch (e) { + setImportStatus('error', 'آدرس URL معتبر نیست.'); + return; + } + + var useTitle = document.getElementById('calc-import-title'); + var autoArchive = document.getElementById('calc-import-auto-archive'); + var existingPageWithUrl = findPageBySourceUrl(url); + var pageNum = getSequentialPageNumber(); + var excludeLayout = pageNum > 1; + + var page = getActivePage(); + if (!page || page.archived) { + var drafts = getDraftPages(); + if (drafts.length) { + activePageId = drafts[0].id; + page = getActivePage(); + } else { + var freshPage = createPageData(getNextDraftPageName(), []); + state.pages.push(freshPage); + activePageId = freshPage.id; + page = freshPage; + } + } + + btn.disabled = true; + setImportStatus('loading', 'در حال دریافت صفحه ' + pageNum + ' و تحلیل بلاک‌ها…' + + (excludeLayout ? ' (بدون هدر/فوتر)' : '')); + + fetchPageHtml(url) + .then(function (html) { + var parsed = resolveUrlAnalysis(url, html); + var matched; + if (pageNum === 1 && existingPageWithUrl) { + matched = matchRowsAsNew(parsed.rows, { excludeLayout: false }); + } else { + matched = matchRowsWithReuse(parsed.rows, { excludeLayout: excludeLayout }); + } + var applied = applyImportedRows(matched.rows, parsed.pageTitle, { + replace: true, + useTitle: useTitle ? useTitle.checked : true, + sourceUrl: url, + projectPageNumber: pageNum + }); + + if (!applied) return; + + var importedPage = getActivePage(); + var shouldArchive = !autoArchive || autoArchive.checked; + if (shouldArchive) { + autoArchiveActivePage(); + } + + setImportStatus('success', buildImportStatusMessage(pageNum, { + page: importedPage, + rows: matched.rows, + stats: matched.stats + }, parsed.meta, shouldArchive)); + + urlInput.value = ''; + urlInput.focus(); + }) + .catch(function () { + setImportStatus('error', 'دریافت صفحه ممکن نشد. ابتدا با npm run dev سرور را اجرا کنید، سپس URL را دوباره امتحان کنید.'); + }) + .finally(function () { + btn.disabled = false; + }); + } + + function initCalculator() { + rowsEl = document.getElementById('calc-rows'); + emptyEl = document.getElementById('calc-empty'); + tabsEl = document.getElementById('calc-page-tabs'); + pagesListEl = document.getElementById('calc-pages-list'); + archiveListEl = document.getElementById('calc-archive-list'); + calcMainEl = document.getElementById('calc-main'); + + if (!rowsEl || !document.getElementById('calc-add')) { + return; + } + + var homePage = createPageData('صفحه ۱', DEFAULT_ROWS); + homePage.projectPageNumber = 1; + state.pages.push(homePage); + activePageId = homePage.id; + + document.getElementById('calc-add').addEventListener('click', function () { + createRow(); + }); + + document.getElementById('calc-reuse').addEventListener('click', addReuseRow); + document.getElementById('calc-page-add').addEventListener('click', addPage); + document.getElementById('calc-page-delete').addEventListener('click', deletePage); + document.getElementById('calc-page-archive').addEventListener('click', archivePage); + document.getElementById('calc-page-unarchive').addEventListener('click', unarchivePage); + var deleteArchiveBtn = document.getElementById('calc-page-delete-archive'); + if (deleteArchiveBtn) { + deleteArchiveBtn.addEventListener('click', deleteArchivedPage); + } + + document.getElementById('calc-page-name').addEventListener('input', recalculate); + + loadProjectTitleFromStorage(); + var projectTitleEl = document.getElementById('opt-project-title'); + if (projectTitleEl) { + projectTitleEl.addEventListener('input', function () { + saveProjectTitleToStorage(); + recalculate(); + }); + } + + ['opt-content', 'opt-responsive', 'opt-dark', 'opt-bilingual', 'opt-animation'].forEach(function (id) { + document.getElementById(id).addEventListener('change', function () { + var wrap = document.getElementById('anim-extra-wrap'); + if (wrap) { + wrap.classList.toggle('hidden', document.getElementById('opt-animation').value !== 'complex'); + } + recalculate(); + }); + }); + + document.getElementById('opt-anim-units').addEventListener('input', recalculate); + var pageMultEl = document.getElementById('opt-page-multiplier'); + if (pageMultEl) pageMultEl.addEventListener('input', recalculate); + var revisionEl = document.getElementById('opt-revision-units'); + if (revisionEl) revisionEl.addEventListener('input', recalculate); + + var importBtn = document.getElementById('calc-import-btn'); + var importUrl = document.getElementById('calc-import-url'); + if (importBtn) { + importBtn.addEventListener('click', importFromUrl); + } + if (importUrl) { + importUrl.addEventListener('keydown', function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + importFromUrl(); + } + }); + importUrl.addEventListener('input', clearImportStatus); + } + + var reportCopyBtn = document.getElementById('calc-report-copy'); + var reportDownloadBtn = document.getElementById('calc-report-download'); + var reportDownloadTxtBtn = document.getElementById('calc-report-download-txt'); + var reportDownloadPdfBtn = document.getElementById('calc-report-download-pdf'); + var reportSelectBtn = document.getElementById('calc-report-select'); + var reportRefreshBtn = document.getElementById('calc-report-refresh'); + var reportBody = getReportBodyEl(); + + if (reportCopyBtn) reportCopyBtn.addEventListener('click', copyProjectReport); + if (reportDownloadBtn) reportDownloadBtn.addEventListener('click', downloadProjectReport); + if (reportDownloadTxtBtn) reportDownloadTxtBtn.addEventListener('click', downloadProjectReportText); + if (reportDownloadPdfBtn) reportDownloadPdfBtn.addEventListener('click', downloadProjectReportPdf); + if (reportSelectBtn) reportSelectBtn.addEventListener('click', selectProjectReport); + if (reportRefreshBtn) reportRefreshBtn.addEventListener('click', refreshProjectReport); + + if (reportBody) { + reportBody.addEventListener('input', onReportEdited); + try { + var savedHtml = localStorage.getItem('feep-price-report-html'); + if (savedHtml) { + reportBody.innerHTML = savedHtml; + state.reportEdited = true; + } + } catch (e) { /* ignore */ } + } + + renderActivePage(); + recalculate(); + } + + function initCollapse() { + document.querySelectorAll('[data-collapse]').forEach(function (el) { + var trigger = el.classList.contains('table-collapse') + ? el.querySelector('.table-collapse-trigger') + : el.querySelector('.section-trigger'); + if (!trigger) return; + trigger.addEventListener('click', function () { + var open = el.classList.toggle('is-open'); + trigger.setAttribute('aria-expanded', open ? 'true' : 'false'); + }); + }); + } + + function init() { + initCalculator(); + initCollapse(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/feep-logo.svg b/feep-logo.svg new file mode 100644 index 0000000..f8d98ee --- /dev/null +++ b/feep-logo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + diff --git a/index.html b/index.html new file mode 100644 index 0000000..19d899c --- /dev/null +++ b/index.html @@ -0,0 +1,2431 @@ + + + + + + تعرفه Blockها | فیپ + + + + + + + + +
+ + + + + +
+ + +
+
+

ماشین‌حساب قیمت

+

لینک هر صفحه را وارد کنید — بلاک‌ها خودکار شناسایی، قیمت‌گذاری و آرشیو می‌شوند. از صفحه دوم به بعد هدر و فوتر حساب نمی‌شود و بلاک‌های مشابه به‌صورت Reuse محاسبه می‌گردند. برای صفحات تکراری (مثلاً ۱۵ دسته‌بندی فروشگاه) یک قالب بسازید و با ضریب تکرار همان صفحه را چندبرابر کنید.

+ +
+
واردات خودکار صفحه‌به‌صفحه
+

آدرس صفحه را وارد کنید. سیستم در ۳ فاز کار می‌کند: ۱) شمارش بلاک‌های ساختاری (Elementor section، header، footer) ۲) شناسایی نوع هر بلاک ۳) قیمت‌گذاری و آرشیو.

+
+ + +
+
+ + +
+ +
+ +
+
+ + +
+
+ +
+ +
+ + + +
+ +
+ دسته + Block + پیچیدگی + وضعیت + Unit + +
+
+
هنوز Blockی اضافه نشده — روی دکمه زیر کلیک کنید.
+
+ + +
+ +
+
تنظیمات این صفحه
+
+
+ + + +
+
+ + +

مثلاً ۱۵ برای ۱۵ دسته‌بندی یکسان — هزینه و Unit این صفحه ×۱۵ می‌شود.

+
+
+
+ +
+ + + +
+ +
+
+ جمع این صفحه + +
+ ۰ تومان +
+
+ + +
+ +
+
+
مجموع Unitها۰
+
هزینه Blockها۰ تومان
+
اضافه محتوا—
+
اضافه Responsive—
+
اضافه Dark Mode—
+
اضافه دوزبانه—
+
Revision اضافه—
+
+
+ قیمت نهایی پروژه + ۰ تومان +
+
+ +
+
+
+
گزارش ذخیره‌شده پروژه
+

گزارش به‌صورت جدول قابل ویرایش است. با «دانلود PDF» همان محتوای نمایش‌داده‌شده در پنجره چاپ باز می‌شود — «ذخیره به PDF» را انتخاب کنید.

+
+
+ + + + + + +
+
+
+ +
+
+
+ +
+ + +
+

قوانین کلی

+

در این سیستم، هر Block بر اساس سه معیار اصلی قیمت‌گذاری می‌شود:

+
    +
  • نوع (Type)
  • +
  • سطح پیچیدگی (Simple / Medium / Complex)
  • +
  • وضعیت استفاده (New / Reuse)
  • +
+ +

به عنوان مثال:

+
+ +
+
+
+ + + + + + + + + +
نمونه بلاکUnit
Hero (Medium)3 Unit
Feature Grid2 Unit
Pricing2 Unit
Navbar1.5 Unit
Footer1 Unit
+
+
+
+
+ +

در صورتی که همین Hero قبلاً طراحی شده باشد:

+
+ +
+
+
+ + + + + +
وضعیتUnit
Reuse Hero0.3 Unit
+
+
+
+
+ +
+ تعرفه هر Unit برابر با ۵۰۰,۰۰۰ تومان است. +
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
BlockSimpleMediumComplex
Navbar11.52
Sidebar123
Footer0.811.5
Hero234
Banner11.52
CTA0.811.5
Feature Grid123
Card Section123
FAQ11.52
Timeline123
Testimonial123
Pricing234
Gallery235
Blog List234
Contact Section123
+
+
+
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + + + +
BlockSimpleMediumComplex
Stat Card0.50.81
Widget123
Data Table246
Filter Bar123
Search0.511.5
Pagination0.50.81
Tabs0.812
Chart235
Calendar246
Kanban468
+
+
+
+
+ + +
+ +
+
+
+ + + + + + + + + + + + +
BlockSimpleMediumComplex
Form Section123
Input Group0.30.51
File Upload123
Rich Text234
Multi Step358
Dynamic Field246
+
+
+
+
+ + +
+ +
+
+
+ + + + + + + + + + + + + + +
BlockUnit
Modal1
Drawer1
Dropdown0.5
Popover0.5
Command Menu1
Toast0.5
Skeleton0.5
Loading State0.5
Empty State0.5
Error State0.5
+
+
+
+
+ + +
+ +
+
+

در صورتی که یک Block قبلاً طراحی و پیاده‌سازی شده باشد، هزینه آن بر اساس میزان تغییرات موردنیاز محاسبه می‌شود.

+ +
+
+ +
+
+
+ + + + + + + + + +
وضعیتدرصد هزینه
استفاده مجدد بدون تغییر10٪
تغییر جزئی25٪
تغییر متوسط40٪
تغییر زیاد70٪
بلاک کاملاً جدید100٪
+
+
+
+
+
+ +
+
+
+ + + + + + + + +
نمونهدرصد هزینه
Navbar100٪
Navbar با منوی متفاوت25٪
Navbar با Mega Menu60٪
Navbar کاملاً جدید100٪
+
+
+
+
+
+
+
+
+ + +
+ +
+
+

تولید یا آماده‌سازی محتوا به صورت جداگانه روی قیمت پروژه تأثیر می‌گذارد.

+
+ + + + + + + + + + +
نوع محتواضریب هزینه
Placeholderرایگان
جایگذاری متن مشتری5٪
برش و بهینه‌سازی تصاویر5٪
تولید محتوا20٪
تولید تصاویر با هوش مصنوعی10٪
طراحی بنر اختصاصیتوافقی
+
+
+
+
+ + +
+ +
+
+

پشتیبانی از دستگاه‌های مختلف به صورت درصدی به هزینه پروژه اضافه می‌شود.

+
+ + + + + + + +
حالتضریب
Desktop فقط100٪
Desktop + Tablet+10٪
Desktop + Tablet + Mobile+20٪
+
+
+
+
+ + +
+ +
+
+

در صورت پیاده‌سازی نسخه دوزبانه سایت، هزینه زیر به پروژه اضافه خواهد شد.

+
+ + + + + +
موردضریب
پیاده‌سازی دوزبانه+10٪
+
+
+
+
+ + +
+ +
+
+

در صورت طراحی نسخه Dark Mode برای کل پروژه، هزینه زیر به پروژه اضافه خواهد شد.

+
+ + + + + +
موردضریب
پیاده‌سازی کامل Dark Mode+15٪
+
+
+
+
+ + +
+ +
+
+

انیمیشن‌ها بر اساس میزان پیچیدگی قیمت‌گذاری می‌شوند.

+
+ + + + + + + + +
نوع انیمیشنUnit
Hover Effectرایگان
Transition0.2
Motion ساده0.5
Motion پیچیده1 تا 3
+
+
+
+
+ + +
+

Revision Policy

+
+
+

موارد رایگان

+
    +
  • دو مرتبه اصلاح برای هر Block قبل از تأیید نهایی
  • +
  • اصلاح رنگ‌ها
  • +
  • اصلاح متن
  • +
  • اصلاح فاصله‌ها
  • +
  • تغییر آیکون‌ها
  • +
  • اصلاح اندازه‌ها
  • +
  • رفع باگ‌های پیاده‌سازی
  • +
+
+
+

موارد مشمول هزینه

+
    +
  • اضافه شدن Block جدید
  • +
  • حذف یک Block و جایگزینی با Block دیگر
  • +
  • تغییر Layout پس از تأیید نهایی
  • +
  • تغییر UX
  • +
  • تغییر Flow صفحات
  • +
  • تغییر Design System پروژه
  • +
+
+
+
+ + +
+

نمونه محاسبه هزینه

+

فرض کنید صفحه Home شامل Blockهای زیر باشد:

+ +
+ +
+
+
+ + + + + + + + + + + + + + + + + + +
BlockUnitهزینه (تومان)
Navbar1.5750,000
Hero31,500,000
Features21,000,000
Statistics1500,000
Testimonials21,000,000
Pricing31,500,000
FAQ1.5750,000
CTA1500,000
Footer1500,000
جمع کل16 Unit8,000,000 تومان
+
+
+
+
+ +

حال فرض کنید در صفحه دوم همان Navbar، Footer و FAQ دوباره استفاده شوند.

+ +
+ +
+
+
+ + + + + + + + + + + + +
BlockUnitهزینه (تومان)
Navbar (Reuse)0.2100,000
Footer (Reuse)0.150,000
FAQ (Reuse)0.3150,000
جمع0.6 Unit300,000 تومان
+
+
+
+
+ +

به این ترتیب، در صفحات بعدی فقط Blockهای جدید یا Blockهایی که نیاز به تغییر دارند هزینه‌گذاری می‌شوند و استفاده مجدد از Blockهای موجود، هزینه بسیار ناچیزی خواهد داشت.

+
+ + +
+

فرمول محاسبه هزینه

+

هزینه هر پروژه بر اساس مجموع Unitهای موردنیاز محاسبه می‌شود.

+ +
+ هزینه کل = مجموع Unit × 500,000 تومان +
+ +

در صورت نیاز، هزینه‌های مربوط به تولید محتوا، طراحی بنر، طراحی Responsive، پیاده‌سازی دوزبانه، پیاده‌سازی Dark Mode، انیمیشن‌ها یا درخواست‌های خارج از محدوده قرارداد به مبلغ نهایی اضافه خواهد شد.

+
+ + +
+

پیشنهاد برای ساختار پروژه

+

برای پروژه‌هایی که با Next.js و shadcn/ui توسعه داده می‌شوند، پیشنهاد می‌شود یک کاتالوگ ثابت از Blockهای استاندارد ایجاد شود.

+ +

این کاتالوگ می‌تواند شامل حدود ۷۰ تا ۱۰۰ Block در دسته‌بندی‌های مختلف مانند:

+ +
    +
  • Landing Page
  • +
  • Dashboard
  • +
  • CRM
  • +
  • ERP
  • +
  • E-commerce
  • +
  • Authentication
  • +
  • Marketing
  • +
  • Blog
  • +
+ +

برای هر Block یک Unit پایه و برای هر سطح پیچیدگی (Simple، Medium و Complex) مقدار مشخصی تعریف می‌شود. سپس برآورد هر پروژه تنها با انتخاب Blockهای موردنیاز انجام خواهد شد.

+ +

در این مدل، صفحه صرفاً محلی برای کنار هم قرار گرفتن Blockها است و نقش مستقیمی در قیمت‌گذاری ندارد. این روش علاوه بر افزایش دقت برآورد، باعث شفافیت بیشتر برای کارفرما، مقیاس‌پذیری بالاتر پروژه‌ها و سرعت بیشتر در تهیه پیش‌فاکتور خواهد شد.

+
+ + +
+

تعرفه پایه

+
+ هر Unit = 500,000 تومان +
+
+ +
+ +
+
+ + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..bd1eac4 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "feep-price-page", + "version": "1.0.0", + "private": true, + "description": "ماشین‌حساب تعرفه Block — فیپ", + "scripts": { + "dev": "node server.js", + "start": "node server.js" + }, + "engines": { + "node": ">=18" + } +} diff --git a/reui-logo.svg b/reui-logo.svg new file mode 100644 index 0000000..1becba2 --- /dev/null +++ b/reui-logo.svg @@ -0,0 +1 @@ +404: Not Found \ No newline at end of file diff --git a/reui-temp.css b/reui-temp.css new file mode 100644 index 0000000..65ad094 --- /dev/null +++ b/reui-temp.css @@ -0,0 +1,25 @@ +@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/53b9e256198e5412-s.390ncx5urfkfu.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/7178b3e590c64307-s.21jp631_3pja2.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/caa3a2e1cccd8315-s.0wgildi0cnwt9.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Fallback;src:local(Arial);ascent-override:95.94%;descent-override:28.16%;line-gap-override:0.0%;size-adjust:104.76%}.geist_b24c070c-module__ta_e_q__className{font-family:Geist,Geist Fallback;font-style:normal}.geist_b24c070c-module__ta_e_q__variable{--font-geist-sans:"Geist", "Geist Fallback"} +@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/2c55a0e60120577a-s.0-dom-5bn10r2.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/9c72aa0f40e4eef8-s.1y4-pdgsjb-pw.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/ad66f9afd8947f86-s.3lvt2whj97whp.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+1F??}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/5476f68d60460930-s.2uwcyprjm3xu3.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/2bbe8d2671613f1f-s.0k62hbripvv8p.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/1bffadaabf893a1e-s.3-6t-g6q0vh0a.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/83afe278b6a6bb3c-s.2bn3s6zvc0dyp.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Inter Fallback;src:local(Arial);ascent-override:90.44%;descent-override:22.52%;line-gap-override:0.0%;size-adjust:107.12%}.inter_86f0310e-module__z5LeqG__className{font-family:Inter,Inter Fallback;font-style:normal}.inter_86f0310e-module__z5LeqG__variable{--font-inter:"Inter", "Inter Fallback"} +@font-face{font-family:Noto Sans;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/d0daeb77ad486505-s.1qp4msj8oy5kg.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Noto Sans;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/13c8bea787795a91-s.1undqm_-sukdc.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Noto Sans;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/fb34704f4f4f68ab-s.3giajfo5uv2ys.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+900-97F,U+1CD0-1CF9,U+200C-200D,U+20A8,U+20B9,U+20F0,U+25CC,U+A830-A839,U+A8E0-A8FF,U+11B00-11B09}@font-face{font-family:Noto Sans;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/26229c3b890d5b41-s.2bg48w3oiort5.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+1F??}@font-face{font-family:Noto Sans;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/ec173da4e4ac482c-s.1f0d9os9qfevp.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Noto Sans;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/eca1cc1e672884b5-s.28-lxb4pyxwvn.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Noto Sans;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/05ce0245cbf07d46-s.1__0ydqd-xg_4.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Noto Sans;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/dc6d2a64e9dbf3bc-s.3mjzw95c0-_52.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Noto Sans Fallback;src:local(Arial);ascent-override:100.54%;descent-override:27.56%;line-gap-override:0.0%;size-adjust:106.33%}.noto_sans_e26db54b-module__duQNOG__className{font-family:Noto Sans,Noto Sans Fallback;font-style:normal}.noto_sans_e26db54b-module__duQNOG__variable{--font-noto-sans:"Noto Sans", "Noto Sans Fallback"} +@font-face{font-family:Nunito Sans;font-style:normal;font-weight:200 1000;font-stretch:100%;font-display:swap;src:url(../media/f813d0b748b050e0-s.109ulqh0wub5u.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:200 1000;font-stretch:100%;font-display:swap;src:url(../media/dcd150d580a10316-s.02m2p-env-7db.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:200 1000;font-stretch:100%;font-display:swap;src:url(../media/d261779aa92de579-s.0slsd4dj30esv.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:200 1000;font-stretch:100%;font-display:swap;src:url(../media/24ce79ed121239f3-s.187m47s3z4syd.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Nunito Sans;font-style:normal;font-weight:200 1000;font-stretch:100%;font-display:swap;src:url(../media/f141b5b7abe57afc-s.380f-mwhk0aej.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Nunito Sans Fallback;src:local(Arial);ascent-override:99.71%;descent-override:34.82%;line-gap-override:0.0%;size-adjust:101.39%}.nunito_sans_a5bd7630-module__sUzrsq__className{font-family:Nunito Sans,Nunito Sans Fallback;font-style:normal}.nunito_sans_a5bd7630-module__sUzrsq__variable{--font-nunito-sans:"Nunito Sans", "Nunito Sans Fallback"} +@font-face{font-family:Figtree;font-style:normal;font-weight:300 900;font-display:swap;src:url(../media/400bf8aa837fcb7e-s.19g-lkcx7iw64.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Figtree;font-style:normal;font-weight:300 900;font-display:swap;src:url(../media/f7aa21714c1c53f8-s.0-95eo-012xnf.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Figtree Fallback;src:local(Arial);ascent-override:94.32%;descent-override:24.82%;line-gap-override:0.0%;size-adjust:100.72%}.figtree_57df2a42-module__C15RPG__className{font-family:Figtree,Figtree Fallback;font-style:normal}.figtree_57df2a42-module__C15RPG__variable{--font-figtree:"Figtree", "Figtree Fallback"} +@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/572c13468b8cff87-s.1rlxnzisjz_e1.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/62d4b50592542c9f-s.1lnyw1zsxhp0r.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/38772d1e0cde909c-s.0lvk2smlqz3yc.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+1F??}@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/a0ffaa6a9384d9d3-s.2dzaija1uhw5g.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/fad568eea626db0e-s.0fd-fjxmcxcjd.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+302-303,U+305,U+307-308,U+310,U+312,U+315,U+31A,U+326-327,U+32C,U+32F-330,U+332-333,U+338,U+33A,U+346,U+34D,U+391-3A1,U+3A3-3A9,U+3B1-3C9,U+3D1,U+3D5-3D6,U+3F0-3F1,U+3F4-3F5,U+2016-2017,U+2034-2038,U+203C,U+2040,U+2043,U+2047,U+2050,U+2057,U+205F,U+2070-2071,U+2074-208E,U+2090-209C,U+20D0-20DC,U+20E1,U+20E5-20EF,U+2100-2112,U+2114-2115,U+2117-2121,U+2123-214F,U+2190,U+2192,U+2194-21AE,U+21B0-21E5,U+21F1-21F2,U+21F4-2211,U+2213-2214,U+2216-22FF,U+2308-230B,U+2310,U+2319,U+231C-2321,U+2336-237A,U+237C,U+2395,U+239B-23B7,U+23D0,U+23DC-23E1,U+2474-2475,U+25AF,U+25B3,U+25B7,U+25BD,U+25C1,U+25CA,U+25CC,U+25FB,U+266D-266F,U+27C0-27FF,U+2900-2AFF,U+2B0E-2B11,U+2B30-2B4C,U+2BFE,U+3030,U+FF5B,U+FF5D,U+1D400-1D7FF,U+1EE??}@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/ccee61546c0358b7-s.42l_3z3944ji5.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+1-C,U+E-1F,U+7F-9F,U+20DD-20E0,U+20E2-20E4,U+2150-218F,U+2190,U+2192,U+2194-2199,U+21AF,U+21E6-21F0,U+21F3,U+2218-2219,U+2299,U+22C4-22C6,U+2300-243F,U+2440-244A,U+2460-24FF,U+25A0-27BF,U+28??,U+2921-2922,U+2981,U+29BF,U+29EB,U+2B??,U+4DC0-4DFF,U+FFF9-FFFB,U+10140-1018E,U+10190-1019C,U+101A0,U+101D0-101FD,U+102E0-102FB,U+10E60-10E7E,U+1D2C0-1D2D3,U+1D2E0-1D37F,U+1F0??,U+1F100-1F1AD,U+1F1E6-1F1FF,U+1F30D-1F30F,U+1F315,U+1F31C,U+1F31E,U+1F320-1F32C,U+1F336,U+1F378,U+1F37D,U+1F382,U+1F393-1F39F,U+1F3A7-1F3A8,U+1F3AC-1F3AF,U+1F3C2,U+1F3C4-1F3C6,U+1F3CA-1F3CE,U+1F3D4-1F3E0,U+1F3ED,U+1F3F1-1F3F3,U+1F3F5-1F3F7,U+1F408,U+1F415,U+1F41F,U+1F426,U+1F43F,U+1F441-1F442,U+1F444,U+1F446-1F449,U+1F44C-1F44E,U+1F453,U+1F46A,U+1F47D,U+1F4A3,U+1F4B0,U+1F4B3,U+1F4B9,U+1F4BB,U+1F4BF,U+1F4C8-1F4CB,U+1F4D6,U+1F4DA,U+1F4DF,U+1F4E3-1F4E6,U+1F4EA-1F4ED,U+1F4F7,U+1F4F9-1F4FB,U+1F4FD-1F4FE,U+1F503,U+1F507-1F50B,U+1F50D,U+1F512-1F513,U+1F53E-1F54A,U+1F54F-1F5FA,U+1F610,U+1F650-1F67F,U+1F687,U+1F68D,U+1F691,U+1F694,U+1F698,U+1F6AD,U+1F6B2,U+1F6B9-1F6BA,U+1F6BC,U+1F6C6-1F6CF,U+1F6D3-1F6D7,U+1F6E0-1F6EA,U+1F6F0-1F6F3,U+1F6F7-1F6FC,U+1F7??,U+1F800-1F80B,U+1F810-1F847,U+1F850-1F859,U+1F860-1F887,U+1F890-1F8AD,U+1F8B0-1F8BB,U+1F8C0-1F8C1,U+1F900-1F90B,U+1F93B,U+1F946,U+1F984,U+1F996,U+1F9E9,U+1FA00-1FA6F,U+1FA70-1FA7C,U+1FA80-1FA89,U+1FA8F-1FAC6,U+1FACE-1FADC,U+1FADF-1FAE9,U+1FAF0-1FAF8,U+1FB??}@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/8fcf71678f67d0c5-s.1szw2e8s5ktgm.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/f7d6090e8b3debad-s.026ae5sgholss.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Roboto;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/ce62453a442c7f35-s.0a0h245ktd4x0.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Roboto Fallback;src:local(Arial);ascent-override:92.98%;descent-override:24.47%;line-gap-override:0.0%;size-adjust:99.78%}.roboto_4cfa3e9-module__6fr2eG__className{font-family:Roboto,Roboto Fallback;font-style:normal}.roboto_4cfa3e9-module__6fr2eG__variable{--font-roboto:"Roboto", "Roboto Fallback"} +@font-face{font-family:Raleway;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/198d17f2fc7b7e24-s.3ln4cacw7vm32.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Raleway;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/bb835a708696869b-s.0_i4fxg1pnle5.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Raleway;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/6c7c54d92ecafd95-s.02co4hodosnv0.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Raleway;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/4b825e536f0add60-s.15vlspr5uap8d.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Raleway;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/c825fd02acae0153-s.1q3-y3vmyf3p6.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Raleway Fallback;src:local(Arial);ascent-override:90.51%;descent-override:22.53%;line-gap-override:0.0%;size-adjust:103.86%}.raleway_5920d140-module__vFhFra__className{font-family:Raleway,Raleway Fallback;font-style:normal}.raleway_5920d140-module__vFhFra__variable{--font-raleway:"Raleway", "Raleway Fallback"} +@font-face{font-family:DM Sans;font-style:normal;font-weight:100 1000;font-display:swap;src:url(../media/c3cb240f9c892514-s.3z_x6eowsw16y.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:DM Sans;font-style:normal;font-weight:100 1000;font-display:swap;src:url(../media/5c285b27cdda1fe8-s.2_mbdogr7ni8i.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:DM Sans Fallback;src:local(Arial);ascent-override:94.9%;descent-override:29.66%;line-gap-override:0.0%;size-adjust:104.53%}.dm_sans_ed1f1058-module__VZVpXG__className{font-family:DM Sans,DM Sans Fallback;font-style:normal}.dm_sans_ed1f1058-module__VZVpXG__variable{--font-dm-sans:"DM Sans", "DM Sans Fallback"} +@font-face{font-family:Public Sans;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/6e8df35dd937fa7a-s.0itc0wjx1mi4q.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Public Sans;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/fc2699ecc8323b38-s.1gwygi6ipeo67.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Public Sans;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/fa0520225c6f3d07-s.33u8lzvd44aqk.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Public Sans Fallback;src:local(Arial);ascent-override:90.59%;descent-override:21.46%;line-gap-override:0.0%;size-adjust:104.87%}.public_sans_7e37f7b-module__dqpJEW__className{font-family:Public Sans,Public Sans Fallback;font-style:normal}.public_sans_7e37f7b-module__dqpJEW__variable{--font-public-sans:"Public Sans", "Public Sans Fallback"} +@font-face{font-family:Outfit;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/b2ea385cb5ae8625-s.1spbknb88wd48.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Outfit;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/1b99372b3eaef0c8-s.1gsd1jahc5dg_.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Outfit Fallback;src:local(Arial);ascent-override:100.18%;descent-override:26.05%;line-gap-override:0.0%;size-adjust:99.82%}.outfit_f9a220d1-module__n_0Jqq__className{font-family:Outfit,Outfit Fallback;font-style:normal}.outfit_f9a220d1-module__n_0Jqq__variable{--font-outfit:"Outfit", "Outfit Fallback"} +@font-face{font-family:Oxanium;font-style:normal;font-weight:200 800;font-display:swap;src:url(../media/784d20fa5d5f4c0e-s.1-ayd17usv4jj.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Oxanium;font-style:normal;font-weight:200 800;font-display:swap;src:url(../media/5fe5f57ee794eff4-s.04t4-lijcsqd8.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Oxanium Fallback;src:local(Arial);ascent-override:76.07%;descent-override:20.22%;line-gap-override:24.07%;size-adjust:103.86%}.oxanium_83970b49-module__2BhvkW__className{font-family:Oxanium,Oxanium Fallback;font-style:normal}.oxanium_83970b49-module__2BhvkW__variable{--font-oxanium:"Oxanium", "Oxanium Fallback"} +@font-face{font-family:Manrope;font-style:normal;font-weight:200 800;font-display:swap;src:url(../media/a342834df7752944-s.3hd2l7bm32o7k.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Manrope;font-style:normal;font-weight:200 800;font-display:swap;src:url(../media/d3fe2f289711ac3f-s.1l2zhvq5eocqf.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Manrope;font-style:normal;font-weight:200 800;font-display:swap;src:url(../media/58c4895d0a0ef7cc-s.2vhvl9vrvk-va.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Manrope;font-style:normal;font-weight:200 800;font-display:swap;src:url(../media/bfc7db5c00d21bc5-s.173xwsdme27n8.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Manrope;font-style:normal;font-weight:200 800;font-display:swap;src:url(../media/6ab0db14f70d8ed6-s.13hnt-xgp82zk.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Manrope;font-style:normal;font-weight:200 800;font-display:swap;src:url(../media/a343f882a40d2cc9-s.1sj6eobyi31rd.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Manrope Fallback;src:local(Arial);ascent-override:103.31%;descent-override:29.07%;line-gap-override:0.0%;size-adjust:103.19%}.manrope_fe14e078-module__duMt0G__className{font-family:Manrope,Manrope Fallback;font-style:normal}.manrope_fe14e078-module__duMt0G__variable{--font-manrope:"Manrope", "Manrope Fallback"} +@font-face{font-family:Space Grotesk;font-style:normal;font-weight:300 700;font-display:swap;src:url(../media/32687112bd2dd8db-s.1gepa_7fcx9fm.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Space Grotesk;font-style:normal;font-weight:300 700;font-display:swap;src:url(../media/28868e710e86be81-s.2eksvhm1z0jwa.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Space Grotesk;font-style:normal;font-weight:300 700;font-display:swap;src:url(../media/0c89a48fa5027cee-s.2cyn07wtgehh0.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Space Grotesk Fallback;src:local(Arial);ascent-override:89.71%;descent-override:26.62%;line-gap-override:0.0%;size-adjust:109.69%}.space_grotesk_a8f3e1ce-module__v4Dp1q__className{font-family:Space Grotesk,Space Grotesk Fallback;font-style:normal}.space_grotesk_a8f3e1ce-module__v4Dp1q__variable{--font-space-grotesk:"Space Grotesk", "Space Grotesk Fallback"} +@font-face{font-family:Montserrat;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/8298327da6d5bcce-s.3q4n78m6muld7.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Montserrat;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/44fac91e6f2d2b1b-s.1d_6dtvtn9g3_.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Montserrat;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/2b402d8aebffd579-s.16nfeub7ja6ze.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Montserrat;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/a88409fdd7dc121c-s.1mvr14jy3vxq7.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Montserrat;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/e8f2fbee2754df70-s.1dqa_6e_ad4sj.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Montserrat Fallback;src:local(Arial);ascent-override:85.79%;descent-override:22.25%;line-gap-override:0.0%;size-adjust:112.83%}.montserrat_4ac93b54-module__J56aNW__className{font-family:Montserrat,Montserrat Fallback;font-style:normal}.montserrat_4ac93b54-module__J56aNW__variable{--font-montserrat:"Montserrat", "Montserrat Fallback"} +@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:100 700;font-stretch:100%;font-display:swap;src:url(../media/7fea77d1d19108bf-s.2y65deuc30pmy.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:100 700;font-stretch:100%;font-display:swap;src:url(../media/1bcd0e671759a44c-s.2qt3tdwt-y9x_.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:100 700;font-stretch:100%;font-display:swap;src:url(../media/e15f58668ad64cb8-s.3cvcemwuzoywn.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:100 700;font-stretch:100%;font-display:swap;src:url(../media/90c51ce8fb754fe8-s.02z5rrbg5nt92.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:100 700;font-stretch:100%;font-display:swap;src:url(../media/57215795b8570f5c-s.2rixgazldzomk.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:IBM Plex Sans;font-style:normal;font-weight:100 700;font-stretch:100%;font-display:swap;src:url(../media/03fc1b4a8d284b5e-s.0wiir8udbzjvx.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:IBM Plex Sans Fallback;src:local(Arial);ascent-override:101.32%;descent-override:27.18%;line-gap-override:0.0%;size-adjust:101.17%}.ibm_plex_sans_28b542ef-module__x1KXWW__className{font-family:IBM Plex Sans,IBM Plex Sans Fallback;font-style:normal}.ibm_plex_sans_28b542ef-module__x1KXWW__variable{--font-ibm-plex-sans:"IBM Plex Sans", "IBM Plex Sans Fallback"} +@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:200 900;font-display:swap;src:url(../media/da8a9dd0d68e512b-s.0n8i_inn0i2i9.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:200 900;font-display:swap;src:url(../media/94b4a5dc1fe84361-s.3wm_n5jv1to7s.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:200 900;font-display:swap;src:url(../media/ff5be760267bb4bc-s.3dkqd4mro2h3_.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+1F??}@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:200 900;font-display:swap;src:url(../media/99596c30eb072d3c-s.2fp9tilo6sy14.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:200 900;font-display:swap;src:url(../media/e5864b32625b6bc3-s.0m7i6b77o5yxd.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:200 900;font-display:swap;src:url(../media/e285ad1a914469f0-s.15241mr5-1s6i.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Source Sans 3";font-style:normal;font-weight:200 900;font-display:swap;src:url(../media/47df9ba1c7236d3b-s.137759vg1sbmi.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:"Source Sans 3 Fallback";src:local(Arial);ascent-override:109.21%;descent-override:42.66%;line-gap-override:0.0%;size-adjust:93.76%}.source_sans_3_f80d556b-module__SShjqa__className{font-family:"Source Sans 3","Source Sans 3 Fallback";font-style:normal}.source_sans_3_f80d556b-module__SShjqa__variable{--font-source-sans-3:"Source Sans 3", "Source Sans 3 Fallback"} +@font-face{font-family:Instrument Sans;font-style:normal;font-weight:400 700;font-stretch:100%;font-display:swap;src:url(../media/c7f47671e39f7787-s.0klc_mi0-5f4_.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Instrument Sans;font-style:normal;font-weight:400 700;font-stretch:100%;font-display:swap;src:url(../media/f06bf9da926bae75-s.2874ccu1_u7jf.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Instrument Sans Fallback;src:local(Arial);ascent-override:94.42%;descent-override:24.33%;line-gap-override:0.0%;size-adjust:102.74%}.instrument_sans_546e9833-module__qzlqsW__className{font-family:Instrument Sans,Instrument Sans Fallback;font-style:normal}.instrument_sans_546e9833-module__qzlqsW__variable{--font-instrument-sans:"Instrument Sans", "Instrument Sans Fallback"} +@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:100 800;font-display:swap;src:url(../media/13bf9871fe164e7f-s.2f7nqdagzwx2-.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:100 800;font-display:swap;src:url(../media/cc545e633e20c56d-s.176arc174-8zp.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:100 800;font-display:swap;src:url(../media/71b036adf157cdcf-s.0bp8oijd_gu96.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:100 800;font-display:swap;src:url(../media/89b21bb081cb7469-s.1fby2rem9ngyr.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:100 800;font-display:swap;src:url(../media/3fe682a82f50d426-s.0vfdmo25voy_0.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:100 800;font-display:swap;src:url(../media/70bc3e132a0a741e-s.3t6q91iet4nsy.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:JetBrains Mono Fallback;src:local(Arial);ascent-override:75.79%;descent-override:22.29%;line-gap-override:0.0%;size-adjust:134.59%}.jetbrains_mono_9705b9b1-module__7svbxW__className{font-family:JetBrains Mono,JetBrains Mono Fallback;font-style:normal}.jetbrains_mono_9705b9b1-module__7svbxW__variable{--font-jetbrains-mono:"JetBrains Mono", "JetBrains Mono Fallback"} +@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/5ce348bf30bf5439-s.31988l_ccedte.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/4fa387ec64143e14-s.2tuy5pz7dlieh.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/6306c77e7c8268e4-s.2dbetqa9o8jxf.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+2000-2001,U+2004-2008,U+200A,U+23B8-23BD,U+2500-259F}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/7d817b4c03b0c5f1-s.1uyisp29ctx0d.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/bbc41e54d2fcbd21-s.1rgnod-3esatf.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Geist Mono;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/797e433ab948586e-s.0r6juujl39pe6.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Geist Mono Fallback;src:local(Arial);ascent-override:74.67%;descent-override:21.92%;line-gap-override:0.0%;size-adjust:134.59%}.geist_mono_9d8416da-module__pmTara__className{font-family:Geist Mono,Geist Mono Fallback;font-style:normal}.geist_mono_9d8416da-module__pmTara__variable{--font-geist-mono:"Geist Mono", "Geist Mono Fallback"} +@font-face{font-family:Noto Serif;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/d829ba9c72f21fd6-s.2-vp0022p7fyq.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Noto Serif;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/cf1514ba67088a5f-s.3z2z-25kqhk1o.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Noto Serif;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/142d1fb481f1ebcd-s.10g5ueymhy-4m.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+1F??}@font-face{font-family:Noto Serif;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/41900da7e74536d5-s.3xqx9yet3wew_.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Noto Serif;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/aeacf25a943e1f19-s.3iywbw035752i.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+302-303,U+305,U+307-308,U+310,U+312,U+315,U+31A,U+326-327,U+32C,U+32F-330,U+332-333,U+338,U+33A,U+346,U+34D,U+391-3A1,U+3A3-3A9,U+3B1-3C9,U+3D1,U+3D5-3D6,U+3F0-3F1,U+3F4-3F5,U+2016-2017,U+2034-2038,U+203C,U+2040,U+2043,U+2047,U+2050,U+2057,U+205F,U+2070-2071,U+2074-208E,U+2090-209C,U+20D0-20DC,U+20E1,U+20E5-20EF,U+2100-2112,U+2114-2115,U+2117-2121,U+2123-214F,U+2190,U+2192,U+2194-21AE,U+21B0-21E5,U+21F1-21F2,U+21F4-2211,U+2213-2214,U+2216-22FF,U+2308-230B,U+2310,U+2319,U+231C-2321,U+2336-237A,U+237C,U+2395,U+239B-23B7,U+23D0,U+23DC-23E1,U+2474-2475,U+25AF,U+25B3,U+25B7,U+25BD,U+25C1,U+25CA,U+25CC,U+25FB,U+266D-266F,U+27C0-27FF,U+2900-2AFF,U+2B0E-2B11,U+2B30-2B4C,U+2BFE,U+3030,U+FF5B,U+FF5D,U+1D400-1D7FF,U+1EE??}@font-face{font-family:Noto Serif;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/5d3604dad620f8b2-s.38lyhzkn9ujp7.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Noto Serif;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/2b4499d915222007-s.1-tyf9-r4gru-.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Noto Serif;font-style:normal;font-weight:100 900;font-stretch:100%;font-display:swap;src:url(../media/b4e85636e2ca4056-s.11bvk_b9f2jcg.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Noto Serif Fallback;src:local(Times New Roman);ascent-override:90.29%;descent-override:24.75%;line-gap-override:0.0%;size-adjust:118.4%}.noto_serif_ead7c690-module__gf22dW__className{font-family:Noto Serif,Noto Serif Fallback;font-style:normal}.noto_serif_ead7c690-module__gf22dW__variable{--font-noto-serif:"Noto Serif", "Noto Serif Fallback"} +@font-face{font-family:Roboto Slab;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/8856956fa550053e-s.405w_vlmjp9ta.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/fe9e8dd4a2ad4385-s.01hh-51_pfq12.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/c492570a33c2511a-s.3kj2sek51yngj.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+1F??}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/2e31362fa3cbeb8b-s.2s5d5z1_g2zwj.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+370-377,U+37A-37F,U+384-38A,U+38C,U+38E-3A1,U+3A3-3FF}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/bd177ace3eee3014-s.42qkliw-l8o5h.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/da2446ec5f26383f-s.3ibh_1qqm0nz2.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:100 900;font-display:swap;src:url(../media/5cf8828ea9d4dda3-s.05uz1l1tir_-_.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Roboto Slab Fallback;src:local(Times New Roman);ascent-override:89.69%;descent-override:23.2%;line-gap-override:0.0%;size-adjust:116.83%}.roboto_slab_69289f85-module__ksP2BG__className{font-family:Roboto Slab,Roboto Slab Fallback;font-style:normal}.roboto_slab_69289f85-module__ksP2BG__variable{--font-roboto-slab:"Roboto Slab", "Roboto Slab Fallback"} +@font-face{font-family:Merriweather;font-style:normal;font-weight:300 900;font-stretch:100%;font-display:swap;src:url(../media/02046ca7e906ed9f-s.3zpf07psjg673.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Merriweather;font-style:normal;font-weight:300 900;font-stretch:100%;font-display:swap;src:url(../media/6bbcb5b36acd717c-s.24zmmel7ed1zj.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Merriweather;font-style:normal;font-weight:300 900;font-stretch:100%;font-display:swap;src:url(../media/0797a9ffe47acb65-s.0die5hfoqv6qs.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Merriweather;font-style:normal;font-weight:300 900;font-stretch:100%;font-display:swap;src:url(../media/7c2c7251ac640798-s.12jw9x3xnpuw8.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Merriweather;font-style:normal;font-weight:300 900;font-stretch:100%;font-display:swap;src:url(../media/248e1dc0efc99276-s.1d6sb5kuiz5lw.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Merriweather Fallback;src:local(Times New Roman);ascent-override:80.59%;descent-override:22.36%;line-gap-override:0.0%;size-adjust:122.09%}.merriweather_9a1c073a-module__L65dqq__className{font-family:Merriweather,Merriweather Fallback;font-style:normal}.merriweather_9a1c073a-module__L65dqq__variable{--font-merriweather:"Merriweather", "Merriweather Fallback"} +@font-face{font-family:Lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(../media/e7150917543fc9da-s.0mybutugvu-lq.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+460-52F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(../media/71fbf9c08529c2a5-s.2fpqrm51ez0iq.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(../media/e9457141811d41ae-s.02frcczqg7k-8.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+302-303,U+305,U+307-308,U+310,U+312,U+315,U+31A,U+326-327,U+32C,U+32F-330,U+332-333,U+338,U+33A,U+346,U+34D,U+391-3A1,U+3A3-3A9,U+3B1-3C9,U+3D1,U+3D5-3D6,U+3F0-3F1,U+3F4-3F5,U+2016-2017,U+2034-2038,U+203C,U+2040,U+2043,U+2047,U+2050,U+2057,U+205F,U+2070-2071,U+2074-208E,U+2090-209C,U+20D0-20DC,U+20E1,U+20E5-20EF,U+2100-2112,U+2114-2115,U+2117-2121,U+2123-214F,U+2190,U+2192,U+2194-21AE,U+21B0-21E5,U+21F1-21F2,U+21F4-2211,U+2213-2214,U+2216-22FF,U+2308-230B,U+2310,U+2319,U+231C-2321,U+2336-237A,U+237C,U+2395,U+239B-23B7,U+23D0,U+23DC-23E1,U+2474-2475,U+25AF,U+25B3,U+25B7,U+25BD,U+25C1,U+25CA,U+25CC,U+25FB,U+266D-266F,U+27C0-27FF,U+2900-2AFF,U+2B0E-2B11,U+2B30-2B4C,U+2BFE,U+3030,U+FF5B,U+FF5D,U+1D400-1D7FF,U+1EE??}@font-face{font-family:Lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(../media/ac34884600cd8d5d-s.2936i88_6qsfd.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+1-C,U+E-1F,U+7F-9F,U+20DD-20E0,U+20E2-20E4,U+2150-218F,U+2190,U+2192,U+2194-2199,U+21AF,U+21E6-21F0,U+21F3,U+2218-2219,U+2299,U+22C4-22C6,U+2300-243F,U+2440-244A,U+2460-24FF,U+25A0-27BF,U+28??,U+2921-2922,U+2981,U+29BF,U+29EB,U+2B??,U+4DC0-4DFF,U+FFF9-FFFB,U+10140-1018E,U+10190-1019C,U+101A0,U+101D0-101FD,U+102E0-102FB,U+10E60-10E7E,U+1D2C0-1D2D3,U+1D2E0-1D37F,U+1F0??,U+1F100-1F1AD,U+1F1E6-1F1FF,U+1F30D-1F30F,U+1F315,U+1F31C,U+1F31E,U+1F320-1F32C,U+1F336,U+1F378,U+1F37D,U+1F382,U+1F393-1F39F,U+1F3A7-1F3A8,U+1F3AC-1F3AF,U+1F3C2,U+1F3C4-1F3C6,U+1F3CA-1F3CE,U+1F3D4-1F3E0,U+1F3ED,U+1F3F1-1F3F3,U+1F3F5-1F3F7,U+1F408,U+1F415,U+1F41F,U+1F426,U+1F43F,U+1F441-1F442,U+1F444,U+1F446-1F449,U+1F44C-1F44E,U+1F453,U+1F46A,U+1F47D,U+1F4A3,U+1F4B0,U+1F4B3,U+1F4B9,U+1F4BB,U+1F4BF,U+1F4C8-1F4CB,U+1F4D6,U+1F4DA,U+1F4DF,U+1F4E3-1F4E6,U+1F4EA-1F4ED,U+1F4F7,U+1F4F9-1F4FB,U+1F4FD-1F4FE,U+1F503,U+1F507-1F50B,U+1F50D,U+1F512-1F513,U+1F53E-1F54A,U+1F54F-1F5FA,U+1F610,U+1F650-1F67F,U+1F687,U+1F68D,U+1F691,U+1F694,U+1F698,U+1F6AD,U+1F6B2,U+1F6B9-1F6BA,U+1F6BC,U+1F6C6-1F6CF,U+1F6D3-1F6D7,U+1F6E0-1F6EA,U+1F6F0-1F6F3,U+1F6F7-1F6FC,U+1F7??,U+1F800-1F80B,U+1F810-1F847,U+1F850-1F859,U+1F860-1F887,U+1F890-1F8AD,U+1F8B0-1F8BB,U+1F8C0-1F8C1,U+1F900-1F90B,U+1F93B,U+1F946,U+1F984,U+1F996,U+1F9E9,U+1FA00-1FA6F,U+1FA70-1FA7C,U+1FA80-1FA89,U+1FA8F-1FAC6,U+1FACE-1FADC,U+1FADF-1FAE9,U+1FAF0-1FAF8,U+1FB??}@font-face{font-family:Lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(../media/e1ccd2766b08c828-s.15gdzqknx46iu.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(../media/507a47c1876d4ec2-s.2qdkzeru_ecot.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Lora;font-style:normal;font-weight:400 700;font-display:swap;src:url(../media/8c2eb9ceedecfc8e-s.23aeddxv5enbo.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Lora Fallback;src:local(Times New Roman);ascent-override:87.33%;descent-override:23.78%;line-gap-override:0.0%;size-adjust:115.2%}.lora_e8659e01-module__N32VRW__className{font-family:Lora,Lora Fallback;font-style:normal}.lora_e8659e01-module__N32VRW__variable{--font-lora:"Lora", "Lora Fallback"} +@font-face{font-family:Playfair Display;font-style:normal;font-weight:400 900;font-display:swap;src:url(../media/65c558afe41e89d6-s.3jppgd0xzx-0d.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+301,U+400-45F,U+490-491,U+4B0-4B1,U+2116}@font-face{font-family:Playfair Display;font-style:normal;font-weight:400 900;font-display:swap;src:url(../media/14e23f9b59180572-s.0t8ljjjn9wtsx.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+102-103,U+110-111,U+128-129,U+168-169,U+1A0-1A1,U+1AF-1B0,U+300-301,U+303-304,U+308-309,U+323,U+329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Playfair Display;font-style:normal;font-weight:400 900;font-display:swap;src:url(../media/b49b0d9b851e4899-s.2zyohmvb-ks7f.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+100-2BA,U+2BD-2C5,U+2C7-2CC,U+2CE-2D7,U+2DD-2FF,U+304,U+308,U+329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Playfair Display;font-style:normal;font-weight:400 900;font-display:swap;src:url(../media/2a65768255d6b625-s.3u4lli0-axodc.woff2?dpl=dpl_4UVYcJ2kLoYi272Zgzpw7NeujgMz)format("woff2");unicode-range:U+??,U+131,U+152-153,U+2BB-2BC,U+2C6,U+2DA,U+2DC,U+304,U+308,U+329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}@font-face{font-family:Playfair Display Fallback;src:local(Times New Roman);ascent-override:97.25%;descent-override:22.56%;line-gap-override:0.0%;size-adjust:111.26%}.playfair_display_4f7ff9c0-module__m6vRLG__className{font-family:Playfair Display,Playfair Display Fallback;font-style:normal}.playfair_display_4f7ff9c0-module__m6vRLG__variable{--font-playfair-display:"Playfair Display", "Playfair Display Fallback"} + diff --git a/server.js b/server.js new file mode 100644 index 0000000..4743331 --- /dev/null +++ b/server.js @@ -0,0 +1,155 @@ +'use strict'; + +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const { URL } = require('url'); + +const PREFERRED_PORT = Number(process.env.PORT) || 3000; +const MAX_PORT_ATTEMPTS = process.env.PORT ? 1 : 10; +const ROOT = __dirname; +const FETCH_TIMEOUT_MS = 25000; + +const MIME_TYPES = { + '.html': 'text/html; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.svg': 'image/svg+xml', + '.json': 'application/json; charset=utf-8', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.webp': 'image/webp', + '.ico': 'image/x-icon', + '.woff2': 'font/woff2' +}; + +function send(res, status, body, headers) { + res.writeHead(status, headers || {}); + res.end(body); +} + +function isValidFetchUrl(raw) { + try { + const parsed = new URL(raw); + return parsed.protocol === 'http:' || parsed.protocol === 'https:'; + } catch (err) { + return false; + } +} + +async function fetchRemotePage(url) { + const controller = new AbortController(); + const timer = setTimeout(function () { + controller.abort(); + }, FETCH_TIMEOUT_MS); + + try { + const response = await fetch(url, { + signal: controller.signal, + redirect: 'follow', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 FeepPricePage/1.0', + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'fa,en-US;q=0.9,en;q=0.8' + } + }); + + if (!response.ok) { + throw new Error('HTTP ' + response.status); + } + + return response.text(); + } finally { + clearTimeout(timer); + } +} + +function serveStatic(req, res) { + const reqUrl = new URL(req.url, 'http://localhost'); + let pathname = decodeURIComponent(reqUrl.pathname); + if (pathname === '/') pathname = '/index.html'; + + const filePath = path.normalize(path.join(ROOT, pathname)); + if (!filePath.startsWith(ROOT)) { + send(res, 403, 'Forbidden'); + return; + } + + fs.readFile(filePath, function (err, data) { + if (err) { + send(res, err.code === 'ENOENT' ? 404 : 500, err.code === 'ENOENT' ? 'Not Found' : 'Server Error'); + return; + } + + const ext = path.extname(filePath).toLowerCase(); + send(res, 200, data, { + 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream', + 'Cache-Control': 'no-cache' + }); + }); +} + +const server = http.createServer(async function (req, res) { + const reqUrl = new URL(req.url, 'http://localhost'); + + if (reqUrl.pathname === '/api/fetch-page' && req.method === 'GET') { + const target = reqUrl.searchParams.get('url'); + if (!target || !isValidFetchUrl(target)) { + send(res, 400, JSON.stringify({ error: 'Invalid URL' }), { + 'Content-Type': 'application/json; charset=utf-8' + }); + return; + } + + try { + const html = await fetchRemotePage(target); + send(res, 200, html, { + 'Content-Type': 'text/html; charset=utf-8', + 'Cache-Control': 'no-store' + }); + } catch (err) { + send(res, 502, JSON.stringify({ + error: err && err.message ? err.message : 'Fetch failed' + }), { + 'Content-Type': 'application/json; charset=utf-8' + }); + } + return; + } + + if (req.method !== 'GET' && req.method !== 'HEAD') { + send(res, 405, 'Method Not Allowed'); + return; + } + + serveStatic(req, res); +}); + +function startServer(port, attemptsLeft) { + function onError(err) { + server.removeListener('listening', onListening); + + if (err.code === 'EADDRINUSE' && attemptsLeft > 1) { + startServer(port + 1, attemptsLeft - 1); + return; + } + + console.error('Failed to start server:', err.message); + process.exit(1); + } + + function onListening() { + server.removeListener('error', onError); + console.log('Feep price page → http://localhost:' + port); + if (port !== PREFERRED_PORT) { + console.log('Port ' + PREFERRED_PORT + ' was busy; using ' + port + ' instead.'); + } + } + + server.once('error', onError); + server.once('listening', onListening); + server.listen(port); +} + +startServer(PREFERRED_PORT, MAX_PORT_ATTEMPTS);