Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions apps/web/app/freelancers/[username]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
import { freelancers } from "../../../lib/mock";

export default function FreelancerProfilePage({ params }: { params: { username: string } }) {
const freelancer = freelancers.find((profile) => profile.username === params.username);

if (!freelancer) {
return (
<section className="card">
<h2>Freelancer not found</h2>
<p>No mock freelancer profile exists for <strong>{params.username}</strong>.</p>
<p>Return to freelancer search to open an available profile.</p>
</section>
);
}

return (
<section className="card">
<h2>Freelancer Profile</h2>
<p>Profile: <strong>{params.username}</strong></p>
<p>Portfolio, reviews, and active proposals appear here.</p>
<h2>{freelancer.username}</h2>
<p><strong>{freelancer.rate}</strong></p>
<p>{freelancer.skills.join(" · ")}</p>
<p>Portfolio, reviews, and active proposals for {freelancer.username} appear here.</p>
</section>
);
}
3 changes: 2 additions & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"scripts": {
"dev": "next dev -p 3000",
"build": "next build",
"start": "next start -p 3000"
"start": "next start -p 3000",
"test": "node --test test/*.test.js"
},
"devDependencies": {
"@types/node": "22.15.19",
Expand Down
70 changes: 70 additions & 0 deletions apps/web/test/freelancer-profile-route.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const test = require("node:test");
const ts = require("typescript");

const WEB_ROOT = path.resolve(__dirname, "..");

function transpileToTemp(sourcePath, tempRoot) {
const relativePath = path.relative(WEB_ROOT, sourcePath);
const outputPath = path.join(tempRoot, relativePath).replace(/\.(tsx|ts)$/, ".js");
const source = fs.readFileSync(sourcePath, "utf8");
const compiled = ts.transpileModule(source, {
compilerOptions: {
esModuleInterop: true,
jsx: ts.JsxEmit.ReactJSX,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020
},
fileName: sourcePath
});

fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, compiled.outputText);
return outputPath;
}

function loadProfilePage() {
const tempRoot = fs.mkdtempSync(path.join(WEB_ROOT, ".tmp-freelancer-profile-route-"));
transpileToTemp(path.join(WEB_ROOT, "lib", "mock.ts"), tempRoot);
const pagePath = transpileToTemp(path.join(WEB_ROOT, "app", "freelancers", "[username]", "page.tsx"), tempRoot);
const Page = require(pagePath).default;
fs.rmSync(tempRoot, { recursive: true, force: true });
return Page;
}

function textFromElement(node) {
if (node === null || node === undefined || typeof node === "boolean") {
return "";
}
if (typeof node === "string" || typeof node === "number") {
return String(node);
}
if (Array.isArray(node)) {
return node.map(textFromElement).join(" ");
}
if (node.props) {
return textFromElement(node.props.children);
}
return "";
}

test("known freelancer usernames render matching mock profile details", () => {
const Page = loadProfilePage();
const renderedText = textFromElement(Page({ params: { username: "maya-dev" } }));

assert.match(renderedText, /maya-dev/);
assert.match(renderedText, /Next\.js/);
assert.match(renderedText, /TypeScript/);
assert.match(renderedText, /\$65\/hr/);
});

test("unknown freelancer usernames render a clear not-found fallback", () => {
const Page = loadProfilePage();
const renderedText = textFromElement(Page({ params: { username: "missing-user" } }));

assert.match(renderedText, /Freelancer not found/i);
assert.match(renderedText, /missing-user/);
assert.doesNotMatch(renderedText, /Portfolio, reviews, and active proposals appear here/);
});
Loading