refactor: remove bundled personal portfolio

This commit is contained in:
2026-09-16 10:15:51 +03:30
parent db8f257a98
commit 76ab2d1078
115 changed files with 102 additions and 5460 deletions

View File

@@ -25,3 +25,6 @@ npm-debug.log*
Dockerfile* Dockerfile*
compose*.yaml compose*.yaml
compose*.yml compose*.yml
portfolio-private-bundle
private-bundle

4
.gitignore vendored
View File

@@ -50,3 +50,7 @@ Desktop.ini
/releases/ /releases/
/data/ /data/
/backups/ /backups/
# Private import bundles belong outside this checkout.
/portfolio-private-bundle/
/private-bundle/

View File

@@ -229,6 +229,5 @@ npm run test:e2e
``` ```
Browser tests provision a temporary owner in a temporary data directory. Their Browser tests provision a temporary owner in a temporary data directory. Their
credentials are test fixtures, not production defaults. The container smoke test credentials are test fixtures, not production defaults. The optional container smoke test
checks generic startup, packaged legacy import, localized pages, and image checks generic startup, owner commands, uploads, and persistence. Run `scripts/smoke-container.sh IMAGE_TAG` against the built image.
optimization. Run `scripts/smoke-container.sh IMAGE_TAG` against the built image.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 905 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 700 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 905 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 978 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 601 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -7,10 +7,24 @@ const cwd = path.resolve('.next/standalone');
const directory = await mkdtemp(path.join(tmpdir(), 'portfolio-e2e-')); const directory = await mkdtemp(path.join(tmpdir(), 'portfolio-e2e-'));
const port = process.env.PLAYWRIGHT_PORT || '43119'; const port = process.env.PLAYWRIGHT_PORT || '43119';
const env = { ...process.env, DATA_DIR: directory, SITE_URL: `http://127.0.0.1:${port}`, HOSTNAME: '127.0.0.1', PORT: port, OWNER_EMAIL: 'e2e@example.com', OWNER_PASSWORD: 'Only-for-isolated-e2e-tests-2026' }; const env = { ...process.env, DATA_DIR: directory, SITE_URL: `http://127.0.0.1:${port}`, HOSTNAME: '127.0.0.1', PORT: port, OWNER_EMAIL: 'e2e@example.com', OWNER_PASSWORD: 'Only-for-isolated-e2e-tests-2026' };
for (const command of ['import-legacy', 'create-owner']) { for (const command of ['init', 'create-owner']) {
const result = spawnSync(process.execPath, ['maintenance.mjs', command], { cwd, env, stdio: 'inherit' }); const result = spawnSync(process.execPath, ['maintenance.mjs', command], { cwd, env, stdio: 'inherit' });
if (result.status !== 0) { await rm(directory, { recursive: true, force: true }); process.exit(result.status || 1); } if (result.status !== 0) { await rm(directory, { recursive: true, force: true }); process.exit(result.status || 1); }
} }
// Generic sample content keeps browser fixtures independent of a private portfolio.
const seeded = spawnSync(process.execPath, ['--import', 'tsx', '-e', `
const { initialize } = require('./src/server/initialize.ts');
const { readContent, saveContent } = require('./src/server/content.ts');
const db = initialize();
saveContent(db, 'languages', { code: 'fa', name: 'فارسی', direction: 'rtl', enabled: true, messages: {} });
const project = readContent(db).entries.find(item => item.kind === 'projects');
project.shared.liveUrl = 'https://example.com';
project.shared.caseStudy.gallery = [1, 2].map(index => ({ image: project.shared.image, alt: 'Sample image ' + index, caption: 'Sample caption ' + index }));
saveContent(db, 'entries', project);
saveContent(db, 'entries', { ...project, id: 'another-project', position: 1, shared: { ...project.shared, title: 'Another project' } });
db.sqlite.close();
`], { cwd: path.resolve('.'), env, stdio: 'inherit' });
if (seeded.status !== 0) { await rm(directory, { recursive: true, force: true }); process.exit(seeded.status || 1); }
const server = spawn(process.execPath, ['server.js'], { cwd, env, stdio: 'inherit' }); const server = spawn(process.execPath, ['server.js'], { cwd, env, stdio: 'inherit' });
process.on('SIGTERM', () => server.kill()); process.on('SIGINT', () => server.kill()); process.on('SIGTERM', () => server.kill()); process.on('SIGINT', () => server.kill());
server.on('exit', async code => { await rm(directory, { recursive: true, force: true }); process.exit(code || 0); }); server.on('exit', async code => { await rm(directory, { recursive: true, force: true }); process.exit(code || 0); });

View File

@@ -1,30 +0,0 @@
// Run only when refreshing the explicit legacy-import fixture, before editing legacy data.
import { mkdirSync, writeFileSync } from 'node:fs';
import { messagesByLocale } from '../src/i18n/content';
import { getProjects } from '../src/data/projects';
import { getExperiences } from '../src/data/experience';
import { getEducation } from '../src/data/education';
import { getMilestones } from '../src/data/milestones';
import { filterTabs, skillsByDiscipline } from '../src/data/skills';
const placeholders: Record<string, string[]> = {
ariaLabel: ['progress'], progressLabel: ['progress'], liveProjectLabel: ['title'],
openGalleryImage: ['caption'], selectGalleryImage: ['index', 'caption'],
imageCounter: ['current', 'total'], logoAlt: ['company'], viewCertificateLabel: ['title'],
};
const fixture = { messages: messagesByLocale, entries: Object.fromEntries(['en', 'fa'].map(locale => [locale, {
projects: getProjects(locale as 'en' | 'fa'), experience: getExperiences(locale as 'en' | 'fa'),
education: [{ id: 'education', ...getEducation(locale as 'en' | 'fa') }],
milestones: getMilestones(locale as 'en' | 'fa'),
skills: filterTabs.map(tab => ({ id: tab.id, label: messagesByLocale[locale as 'en' | 'fa'].skills.filterLabels[tab.id], ...skillsByDiscipline[tab.id] })),
}])) };
mkdirSync('src/seed', { recursive: true });
writeFileSync('src/seed/legacy.json', JSON.stringify(fixture, (key, value) => {
if (typeof value !== 'function') return value;
const names = placeholders[key];
let result = value(...names.map((name, index) => ['progress', 'index', 'current', 'total'].includes(name) ? 98760 + index : `{${name}}`));
names.forEach((name, index) => {
result = result.replaceAll(String(98760 + index), `{${name}}`).replaceAll(new Intl.NumberFormat('fa-IR', { useGrouping: false }).format(98760 + index), `{${name}}`);
});
return result;
}, 2) + '\n');

View File

@@ -40,7 +40,7 @@ fi
docker exec "$container_name" node -e "fetch('http://127.0.0.1:3000/').then(async r=>{if(!r.ok||!(await r.text()).includes('Your Name'))process.exit(1)})" docker exec "$container_name" node -e "fetch('http://127.0.0.1:3000/').then(async r=>{if(!r.ok||!(await r.text()).includes('Your Name'))process.exit(1)})"
# Exercise the packaged maintenance command and native SQLite driver in the image. # Exercise the packaged maintenance command and native SQLite driver in the image.
docker exec "$container_name" node maintenance.mjs import-legacy docker exec "$container_name" node maintenance.mjs init
docker exec --env OWNER_EMAIL=smoke@example.com --env OWNER_PASSWORD=Isolated-container-smoke-password-2026 "$container_name" node maintenance.mjs create-owner docker exec --env OWNER_EMAIL=smoke@example.com --env OWNER_PASSWORD=Isolated-container-smoke-password-2026 "$container_name" node maintenance.mjs create-owner
docker exec -i "$container_name" node <<'NODE' docker exec -i "$container_name" node <<'NODE'
@@ -53,26 +53,16 @@ async function expectOk(path, options) {
} }
const english = await expectOk('/'); const english = await expectOk('/');
await expectOk('/fa'); await expectOk('/projects/sample-projects');
await expectOk('/projects/interanet-solutions');
await expectOk('/fa/projects/interanet-solutions');
const html = await english.text(); const html = await english.text();
const staticAsset = html.match(/(?:src|href)="(\/_next\/static\/[^"?#]+)/)?.[1]; const staticAsset = html.match(/(?:src|href)="(\/_next\/static\/[^"?#]+)/)?.[1];
if (!staticAsset) throw new Error('No built static asset was found in the home page.'); if (!staticAsset) throw new Error('No built static asset was found in the home page.');
await expectOk(staticAsset); await expectOk(staticAsset);
const optimized = await expectOk(
'/_next/image?url=%2Fimages%2Favatar.png&w=256&q=75', await expectOk('/images/portfolio-placeholder.svg');
{ headers: { Accept: 'image/webp' } },
);
const contentType = optimized.headers.get('content-type') ?? '';
if (!/^image\/(?:webp|png)$/.test(contentType)) {
throw new Error(`Unexpected optimized image type: ${contentType}`);
}
if ((await optimized.arrayBuffer()).byteLength === 0) {
throw new Error('Optimized image response was empty.');
}
NODE NODE
# Upload through the real authenticated endpoint, then replace the container. # Upload through the real authenticated endpoint, then replace the container.
@@ -103,7 +93,7 @@ for (let attempt = 0; attempt < 30; attempt++) {
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
} }
if (!ready) throw new Error('Replacement container did not start'); if (!ready) throw new Error('Replacement container did not start');
const page = await fetch(`${origin}/projects/interanet-solutions`); const page = await fetch(`${origin}/projects/sample-projects`);
if (!page.ok) throw new Error('Database did not survive container replacement'); if (!page.ok) throw new Error('Database did not survive container replacement');
const key = readFileSync('/data/smoke-media-key', 'utf8'); const key = readFileSync('/data/smoke-media-key', 'utf8');
const media = await fetch(`${origin}/media/${key}`); const media = await fetch(`${origin}/media/${key}`);

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