Initial commit: Feep Block pricing calculator with page multiplier
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
+2679
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 27.4.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 69.9 34.2" style="enable-background:new 0 0 69.9 34.2;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M63.5,9.2c-1.2,0-2.5-0.1-3.8-0.1c-2.2,0.3-3.5,2-4,4c0,0.1,0,0.2-0.1,0.3c-0.3,1-0.5,2-0.8,3.1
|
||||
c-6.4,0.3-6.5-0.8-6.1-2.4c0-0.1,0.1-0.2,0.1-0.3c0.4-1.6,0.3-1.3,0.5-2.3c0.1-0.2,0.1-0.5,0.1-0.7c0-0.8-0.1-1.4-0.5-2
|
||||
c-0.4-0.5-0.9-1-1.6-1.3c-1-0.5-1.5-0.3-1.7,0.8c0,0.2-0.1,0.4-0.1,0.7C45.1,10.4,44,15,44,15c-0.4,1.3-0.8,1.5-2.6,1.5
|
||||
c-1.2,0-5.3,0-5.3,0c-1.5-0.2-2.2-0.9-1.9-2.4c0-0.1,0.1-0.2,0.1-0.3c0.4-1.6,0.3-1.3,0.5-2.3c0.1-0.2,0.1-0.5,0.1-0.7
|
||||
c0-0.8-0.1-1.4-0.5-2c-0.4-0.5-0.9-1-1.6-1.3c-1-0.5-1.5-0.3-1.7,0.8c0,0.2-0.1,0.4-0.1,0.7c-0.3,1.5-1.4,6.1-1.4,6.1
|
||||
c-0.4,1.3-0.8,1.5-2.6,1.5c-3.4-0.1-17.9,0.1-21.2,0c-2.5-0.1-3.3,1.4-3.7,2.9l-0.5,1.8c4.6,0.1,27.4,0,27.5,0
|
||||
c0.5,0,0.9-0.1,1.3-0.3c0.7-0.2,1.2-0.6,1.7-1.1c0.6,0.6,1.4,1,2.3,1.2c0.1,0,9.1,0.1,9.2,0.1c1.2-0.1,2.2-0.6,2.9-1.3
|
||||
c0.7,0.7,1.6,1.1,2.7,1.2c0,0,13.7,0.2,13.9,0.2c1.7-0.1,2.5-1.1,3.1-3c0.1-0.5,0.3-0.9,0.4-1.3c0.3-1,0.6-1.9,0.8-2.9
|
||||
c0-0.1,0-0.2,0-0.2C67.9,11.1,66.4,9.3,63.5,9.2z M63,14.2c-0.6,2.4-0.3,2.4-2.6,2.3c-0.2,0-0.2,0-0.5,0c-0.8,0-0.6-0.6-0.6-0.8
|
||||
c0.4-1.9,0.8-2.2,2.7-2.2c0.3,0,1.1,0,1.1,0S63.1,13.9,63,14.2z"/>
|
||||
<g>
|
||||
<path class="st0" d="M17.9,24.3c-2.9,0-5,0-7.6,0c-2.6,0-2.9,1.5-3.3,2.6l-0.6,1.7c2.7,0,5.5,0,8.2,0c0.7,0,1.4,0,2.2,0
|
||||
c0.6,0,0.9,0.2,0.7,0.8l-1.1,3.3c0.5,0,1.5,0,2,0c2.2,0,2.6-1,3.2-3.1C23.2,25.3,20.9,24.2,17.9,24.3z"/>
|
||||
</g>
|
||||
<path class="st0" d="M64,5.9l-2.7-0.2c-0.5-0.1-0.8-0.6-0.7-1.1l0.6-2.4c0.1-0.5,0.6-0.8,1.1-0.7l2.7,0.2c0.5,0.1,0.8,0.6,0.7,1.1
|
||||
l-0.6,2.4C65,5.7,64.5,6,64,5.9z"/>
|
||||
<g>
|
||||
<path class="st0" d="M42.1,24.7c-0.9,0-5.7,0-8.3,0c-1.8,0-1.9,1.3-1.9,1.3l-0.7,3.2l7.8,0c2.3,0,4-1.1,4.3-3.4
|
||||
C43.3,25.1,43,24.7,42.1,24.7z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
+2431
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
404: Not Found
|
||||
File diff suppressed because one or more lines are too long
@@ -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);
|
||||
Reference in New Issue
Block a user