+
+
+
+ resume ↗
+
>
);
};
diff --git a/src/styles/global.css b/src/styles/global.css
index 6f42924..fecb478 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -27,15 +27,109 @@ div#__next > div {
}
::-webkit-scrollbar-track {
- background: #1e252e;
+ background: rgba(0, 0, 0, 0.15);
border-radius: 5px;
}
::-webkit-scrollbar-thumb {
- background: #ebdbb2;
+ background: rgba(154, 205, 50, 0.5);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
- background: #ff8037;
+ background: rgba(154, 205, 50, 0.75);
+}
+
+/* --- Prompt line --- */
+.terminal-current-line {
+ padding: 2px 0;
+}
+
+.ps1-separator {
+ color: rgba(154, 205, 50, 0.85);
+ margin: 0 2px;
+}
+
+.ps1-host {
+ color: #a4ffce;
+}
+
+.ps1-path {
+ color: rgba(186, 255, 216, 0.85);
+ margin-left: 4px;
+}
+
+/* --- Clickable elements, modern-terminal style: underline on hover --- */
+.term-link {
+ color: inherit;
+ text-decoration: none;
+ border-bottom: 1px dotted currentColor;
+ cursor: pointer;
+}
+.term-link:hover {
+ text-decoration: none;
+ border-bottom-style: solid;
+ filter: brightness(1.25);
+}
+
+/* --- Tree listing (ls) --- */
+.tree {
+ white-space: pre-wrap;
+}
+.tree-branch {
+ color: rgba(154, 205, 50, 0.65);
+}
+.tree-row {
+ display: inline;
+ padding: 0 3px;
+ border-radius: 3px;
+}
+.tree-row:hover {
+ background: rgba(154, 205, 50, 0.12);
+}
+
+/* --- sumfetch: flat text summary --- */
+.sumfetch {
+ white-space: pre-wrap;
+ margin: 0;
+ line-height: 1.35;
+}
+.sf-name {
+ font-weight: 700;
+ font-size: 1.05em;
+ color: #b6ff66;
+}
+.sf-title {
+ color: rgba(154, 205, 50, 0.85);
+ font-weight: 700;
+}
+.sf-key {
+ color: #9eff88;
+}
+.sf-link {
+ color: inherit;
+ text-decoration: none;
+ border-bottom: 1px dotted currentColor;
+}
+.sf-link:hover {
+ border-bottom-style: solid;
+ filter: brightness(1.25);
+}
+
+/* --- Floating resume button --- */
+.resume-fab {
+ position: fixed;
+ bottom: 1rem;
+ right: 1rem;
+ z-index: 50;
+ font-size: 0.75rem;
+ padding: 0.375rem 0.75rem;
+ border: 1px solid rgba(154, 205, 50, 0.6);
+ color: rgba(186, 255, 216, 0.9);
+ text-decoration: none;
+ transition: background-color 0.15s ease, color 0.15s ease;
+}
+.resume-fab:hover {
+ background: rgba(154, 205, 50, 0.85);
+ color: #06210f;
}
diff --git a/src/utils/api.ts b/src/utils/api.ts
index 6840cac..2d4f3b7 100644
--- a/src/utils/api.ts
+++ b/src/utils/api.ts
@@ -1,9 +1,10 @@
import axios from 'axios';
import config from '../../config.json';
+import { profile } from './resume';
export const getProjects = async () => {
const { data } = await axios.get(
- `https://api.github.com/users/${config.social.github}/repos`,
+ `https://api.github.com/users/${profile.github}/repos`,
);
return data;
};
diff --git a/src/utils/bin/api_commands.ts b/src/utils/bin/api_commands.ts
deleted file mode 100644
index ce4d487..0000000
--- a/src/utils/bin/api_commands.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-// // List of commands that require API calls
-
-import { getProjects } from '../api';
-import { getQuote } from '../api';
-import { getReadme } from '../api';
-import { getWeather } from '../api';
-
-export const projects = async (args: string[]): Promise
=> {
- const projects = await getProjects();
- return projects
- .map(
- (repo) =>
- `${repo.name} - ${repo.html_url}`,
- )
- .join('\n');
-};
-
-export const quote = async (args: string[]): Promise => {
- const data = await getQuote();
- return data.quote;
-};
-
-export const readme = async (args: string[]): Promise => {
- const readme = await getReadme();
- return `Opening GitHub README...\n
- ${readme}`;
-};
-
-export const weather = async (args: string[]): Promise => {
- const city = args.join('+');
- if (!city) {
- return 'Usage: weather [city]. Example: weather casablanca';
- }
- const weather = await getWeather(city);
- return weather;
-};
diff --git a/src/utils/bin/commands.ts b/src/utils/bin/commands.ts
index 338c9f8..714f7ea 100644
--- a/src/utils/bin/commands.ts
+++ b/src/utils/bin/commands.ts
@@ -1,76 +1,175 @@
-// List of commands that do not require API calls
-
-import * as bin from './index';
import config from '../../../config.json';
+import {
+ profile,
+ experience,
+ projects,
+ education,
+ organizations,
+ publications,
+ skills,
+ catLabel,
+} from '../resume';
+import { getProjects, getQuote, getReadme, getWeather } from '../api';
+
+const b = (s: string) => ` • ${s}`;
+const link = (href: string, text: string) =>
+ `${text}`;
+
+// Turn "arXiv:2507.01246" mentions into a clickable link, in place.
+const linkifyArxiv = (s: string) =>
+ s.replace(/arXiv:(\d{4}\.\d{4,5})/gi, (_m, id) =>
+ link(`https://arxiv.org/abs/${id}`, `arXiv:${id}`),
+ );
+
+export const help = async (_args: string[]): Promise => {
+ const cmds: [string, string][] = [
+ ['about', 'short bio'],
+ [
+ 'ls [section]',
+ 'browse résumé — experience projects education skills organizations publications',
+ ],
+ ['summary', 'full text profile'],
+ ['sumfetch', 'profile card'],
+ ['resume', 'open résumé page in a new tab'],
+ ['email', 'open mailto link'],
+ ['linkedin', 'open LinkedIn profile'],
+ ['github', 'open GitHub profile'],
+ ['github_projects', 'list public GitHub repos'],
+ ['weather [city]', 'current weather'],
+ ['quote', 'random quote'],
+ ['clear', 'clear the terminal'],
+ ];
+ const rows = cmds
+ .map(
+ ([cmd, desc], i) =>
+ `${i === cmds.length - 1 ? '└' : '├'} ${cmd.padEnd(18)}${desc}`,
+ )
+ .join('\n');
+ return `Commands:\n${rows}\n\n[tab] complete [ctrl+l] clear [↑/↓] history`;
+};
+
+export const about = async (_args: string[]): Promise => {
+ return `${profile.name}\n${profile.bio}\n\ntype 'sumfetch' for the profile card, 'ls' to browse sections`;
+};
+
+const SECTIONS = [
+ ['experience', 'professional history'],
+ ['projects', 'things built'],
+ ['education', 'degrees & coursework'],
+ ['organizations', 'leadership roles'],
+ ['publications', 'papers & writing'],
+ ['skills', 'technical stack'],
+] as const;
-// Help
-export const help = async (args: string[]): Promise => {
- const commands = Object.keys(bin).sort().join(', ');
- var c = '';
- for (let i = 1; i <= Object.keys(bin).sort().length; i++) {
- if (i % 7 === 0) {
- c += Object.keys(bin).sort()[i - 1] + '\n';
- } else {
- c += Object.keys(bin).sort()[i - 1] + ' ';
- }
+export const ls = async (args: string[]): Promise => {
+ const section = (args[0] ?? '').toLowerCase().replace(/\/$/, '');
+
+ if (!section) {
+ const rows = SECTIONS.map(([name, desc], i) => {
+ const branch = i === SECTIONS.length - 1 ? '└' : '├';
+ return `${branch} ${name.padEnd(
+ 15,
+ )}${desc}`;
+ }).join('\n');
+ return `~/\n${rows}\n\nuse 'ls <section>' to read, e.g. 'ls experience'`;
}
- return `Welcome! Here are all the available commands:
-\n${c}\n
-[tab]: trigger completion.
-[ctrl+l]/clear: clear terminal.\n
-Type 'sumfetch' to display summary.
-`;
-};
-
-// Redirection
-export const repo = async (args: string[]): Promise => {
- window.open(`${config.repo}`);
- return 'Opening Github repository...';
-};
-// About
-export const about = async (args: string[]): Promise => {
- return `Hi, I am ${config.name}.
-Welcome to my website!
-More about me:
-'sumfetch' - short summary.
-'resume' - my latest resume.
-'readme' - my github readme.`;
+ switch (section) {
+ case 'about':
+ return `${profile.name}\n${profile.bio}`;
+
+ case 'experience':
+ return experience
+ .map(
+ (e) =>
+ `${e.role} @ ${e.company} (${e.dates})\n${e.bullets
+ .map(b)
+ .join('\n')}`,
+ )
+ .join('\n\n');
+
+ case 'projects':
+ return projects
+ .map(
+ (p) =>
+ `${p.name} [${p.timeline}]\n${p.bullets
+ .map((x) => b(linkifyArxiv(x)))
+ .join('\n')}`,
+ )
+ .join('\n\n');
+
+ case 'education':
+ return education
+ .map((e) =>
+ [
+ e.school,
+ e.degrees.map((d) => ` ${d}`).join('\n'),
+ ` GPA: ${e.gpa} · Graduation: ${e.graduation}`,
+ '',
+ 'Honors:',
+ e.honors.map(b).join('\n'),
+ '',
+ 'Coursework:',
+ e.coursework.map(b).join('\n'),
+ ].join('\n'),
+ )
+ .join('\n\n');
+
+ case 'organizations':
+ return organizations
+ .map(
+ (o) =>
+ `${o.role} @ ${o.org} (${o.dates})\n${o.bullets
+ .map(b)
+ .join('\n')}`,
+ )
+ .join('\n\n');
+
+ case 'publications':
+ return publications
+ .map(
+ (p) =>
+ `${p.title}\n ${linkifyArxiv(p.status)}\n${p.bullets
+ .map((x) => b(linkifyArxiv(x)))
+ .join('\n')}`,
+ )
+ .join('\n\n');
+
+ case 'skills':
+ return Object.entries(skills)
+ .map(
+ ([cat, items]) =>
+ `${(catLabel(cat) + ':').padEnd(13)}${items.join(', ')}`,
+ )
+ .join('\n');
+
+ default:
+ return `ls: '${section}' not found\nSections: ${SECTIONS.map(
+ ([n]) => n,
+ ).join(', ')}`;
+ }
};
-export const resume = async (args: string[]): Promise => {
- window.open(`${config.resume_url}`);
- return 'Opening resume...';
+export const resume = async (_args: string[]): Promise => {
+ window.open(config.resume_url);
+ return 'Opening résumé...';
};
-// Donate
-export const donate = async (args: string[]): Promise => {
- return `thank you for your interest.
-here are the ways you can support my work:
-- paypal
-- patreon
-`;
+export const email = async (_args: string[]): Promise => {
+ window.open(`mailto:${profile.email}`);
+ return `Opening mailto:${profile.email}...`;
};
-// Contact
-export const email = async (args: string[]): Promise => {
- window.open(`mailto:${config.email}`);
- return `Opening mailto:${config.email}...`;
+export const github = async (_args: string[]): Promise => {
+ window.open(`https://github.com/${profile.github}/`);
+ return 'Opening GitHub...';
};
-export const github = async (args: string[]): Promise => {
- window.open(`https://github.com/${config.social.github}/`);
-
- return 'Opening github...';
-};
-
-export const linkedin = async (args: string[]): Promise => {
- window.open(`https://www.linkedin.com/in/${config.social.linkedin}/`);
-
- return 'Opening linkedin...';
+export const linkedin = async (_args: string[]): Promise => {
+ window.open(`https://www.linkedin.com/in/${profile.linkedin}/`);
+ return 'Opening LinkedIn...';
};
-// Search
export const google = async (args: string[]): Promise => {
window.open(`https://google.com/search?q=${args.join(' ')}`);
return `Searching google for ${args.join(' ')}...`;
@@ -81,77 +180,54 @@ export const duckduckgo = async (args: string[]): Promise => {
return `Searching duckduckgo for ${args.join(' ')}...`;
};
-export const bing = async (args: string[]): Promise => {
- window.open(`https://bing.com/search?q=${args.join(' ')}`);
- return `Wow, really? You are using bing for ${args.join(' ')}?`;
-};
-
export const reddit = async (args: string[]): Promise => {
window.open(`https://www.reddit.com/search/?q=${args.join(' ')}`);
return `Searching reddit for ${args.join(' ')}...`;
};
-// Typical linux commands
-export const echo = async (args: string[]): Promise => {
- return args.join(' ');
-};
-
-export const whoami = async (args: string[]): Promise => {
- return `${config.ps1_username}`;
-};
+export const echo = async (args: string[]): Promise => args.join(' ');
-export const ls = async (args: string[]): Promise => {
- return `a
-bunch
-of
-fake
-directories`;
-};
+export const whoami = async (_args: string[]): Promise =>
+ config.ps1_username;
-export const cd = async (args: string[]): Promise => {
- return `unfortunately, i cannot afford more directories.
-if you want to help, you can type 'donate'.`;
-};
+export const date = async (_args: string[]): Promise =>
+ new Date().toString();
-export const date = async (args: string[]): Promise => {
- return new Date().toString();
-};
+export const vi = async (_args: string[]): Promise =>
+ `woah, you still use 'vi'? just try 'vim'.`;
+export const vim = async (_args: string[]): Promise =>
+ `'vim' is so outdated. how about 'nvim'?`;
+export const nvim = async (_args: string[]): Promise =>
+ `'nvim'? too fancy. why not 'emacs'?`;
+export const emacs = async (_args?: string[]): Promise =>
+ `you know what? just use vscode.`;
-export const vi = async (args: string[]): Promise => {
- return `woah, you still use 'vi'? just try 'vim'.`;
+export const sudo = async (_args?: string[]): Promise => {
+ window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank');
+ return `Permission denied: with little power comes... no responsibility?`;
};
-export const vim = async (args: string[]): Promise => {
- return `'vim' is so outdated. how about 'nvim'?`;
-};
+// --- API commands ---
-export const nvim = async (args: string[]): Promise => {
- return `'nvim'? too fancy. why not 'emacs'?`;
+export const github_projects = async (_args: string[]): Promise => {
+ const repos = await getProjects();
+ return repos
+ .map((repo: any) => `${repo.name} — ${link(repo.html_url, repo.html_url)}`)
+ .join('\n');
};
-export const emacs = async (args?: string[]): Promise => {
- return `you know what? just use vscode.`;
+export const quote = async (_args: string[]): Promise => {
+ const data = await getQuote();
+ return data.quote;
};
-export const sudo = async (args?: string[]): Promise => {
- window.open('https://www.youtube.com/watch?v=dQw4w9WgXcQ', '_blank'); // ...I'm sorry
- return `Permission denied: with little power comes... no responsibility? `;
+export const readme = async (_args: string[]): Promise => {
+ const data = await getReadme();
+ return `Opening GitHub README...\n\n${data}`;
};
-// Banner
-export const banner = (args?: string[]): string => {
- return `
-█████ ███ ███████████
-░░███ ░░░ ░█░░░███░░░█
- ░███ ████ █████ █████ ██████ ░ ░███ ░ ██████ ████████ █████████████
- ░███ ░░███ ░░███ ░░███ ███░░███ ░███ ███░░███░░███░░███░░███░░███░░███
- ░███ ░███ ░███ ░███ ░███████ ░███ ░███████ ░███ ░░░ ░███ ░███ ░███
- ░███ █ ░███ ░░███ ███ ░███░░░ ░███ ░███░░░ ░███ ░███ ░███ ░███
- ███████████ █████ ░░█████ ░░██████ █████ ░░██████ █████ █████░███ █████
-░░░░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░ ░░░░░ ░░░ ░░░░░
-
-Type 'help' to see the list of available commands.
-Type 'sumfetch' to display summary.
-Type 'repo' or click here for the Github repository.
-`;
+export const weather = async (args: string[]): Promise => {
+ const city = args.join('+');
+ if (!city) return 'Usage: weather [city]. Example: weather london';
+ return getWeather(city);
};
diff --git a/src/utils/bin/index.ts b/src/utils/bin/index.ts
index 0d55ba1..9cf48c0 100644
--- a/src/utils/bin/index.ts
+++ b/src/utils/bin/index.ts
@@ -1,3 +1,2 @@
export * from './commands';
-export * from './api_commands';
-export { default as sumfetch } from './sumfetch';
+export * from './sumfetch';
diff --git a/src/utils/bin/sumfetch.ts b/src/utils/bin/sumfetch.ts
index e55686e..2a99372 100644
--- a/src/utils/bin/sumfetch.ts
+++ b/src/utils/bin/sumfetch.ts
@@ -1,46 +1,152 @@
import config from '../../../config.json';
+import {
+ profile,
+ experience,
+ projects,
+ education,
+ organizations,
+ publications,
+ skills,
+ catLabel,
+} from '../resume';
-const sumfetch = async (args: string[]): Promise => {
- if (config.ascii === 'cveinnt') {
- return `
- @@@@@@@@@@@@@ sumfetch: summary display
- @@@@ @@@@ -----------
- @@ @@ ABOUT
- @@ @@ ${config.name}
- @@ @@ ﰩ ${config.ps1_hostname}
- @@ @@@ @@ resume
-@@ @@@ @@ 爵 Github repo
-@@ @@ -----------
-@@ .@@@@@@@@@@. @@ CONTACT
- @@ @@ @@ @@ ${config.email}
- @@ @@ @@ @@ github.com/${config.social.github}
- @@ @@@@@@ @@ linkedin.com/in/${config.social.linkedin}
- @@@ @@@ -----------
- @@@ @@@ @@ DONATE
- @| @@@@@@@@@@@@@@@@ @@ ${config.donate_urls.paypal}
- @| @@ ${config.donate_urls.patreon}
-
-`;
- } else {
- return `
- ▄▓▓▓▓▓▓▓▓▓▓▓▓▓▓▄ sumfetch
- ▄▓▓▀ ▄▓▓▀▓▓▓▀▓▓▄ ▀▀▓▓▄ -----------
- ▓▓▀ ▄▓▀ ▐▓▓ ▀▓▓ ▓▓▄ ABOUT
- ▄▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ${config.name}
- ▓▓ ▓▓▓ ▐▓▓ ▐▓▓ ▓▓ resume
-▐▓▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▓ 爵 Github repo
-▐▓ ▐▓ -----------
-▐▓ > L I V E T E R M ▐▓ CONTACT
-▐▓ ▐▓ ${config.email}
-▐▓▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▓ github.com/${config.social.github}
- ▓▓ ▐▓▓ ▓▓ ▐▓▓ ▓▓ linkedin.com/in/${config.social.linkedin}
- ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ -----------
- ▓▓▓ ▐▓▓ ▓▓ ▓▓▓ ▓▓▀ DONATE
- ▀▓▓▄▄ ▀▓▓▄▓▓▄▓▓▓▄▄▓▓▀ ${config.donate_urls.paypal}
- ▀▓▓▓▓▓▓▓▓▓▓▓▀▀ ${config.donate_urls.patreon}
-
-`;
- }
+const key = (k: string, w = 11) => `${k.padEnd(w)}`;
+const title = (t: string) => `${t}`;
+const sfLink = (href: string, text: string) =>
+ `${text}`;
+
+// --- sumfetch: clean flat profile card (the landing screen) ---
+const sumfetch = async (_args: string[]): Promise => {
+ const edu = education[0] ?? ({} as any);
+ const contact = [
+ ` ${key('email')}${sfLink(`mailto:${profile.email}`, profile.email)}`,
+ ` ${key('github')}${sfLink(
+ `https://github.com/${profile.github}`,
+ `github.com/${profile.github}`,
+ )}`,
+ ` ${key('linkedin')}${sfLink(
+ `https://linkedin.com/in/${profile.linkedin}`,
+ `linkedin.com/in/${profile.linkedin}`,
+ )}`,
+ profile.phone ? ` ${key('phone')}${profile.phone}` : '',
+ ].filter(Boolean);
+
+ const eduBlock = [
+ ` ${edu.school}`,
+ ...(edu.degrees ?? []).map((d: string) => ` ${d}`),
+ ` GPA ${edu.gpa} · Graduation ${edu.graduation}`,
+ ];
+
+ const expBlock = experience.map(
+ (e) => ` ${e.role} @ ${e.company} (${e.dates})`,
+ );
+ const projBlock = projects.slice(0, 4).map((p) => ` ${p.name}`);
+ const skillBlock = Object.entries(skills).map(
+ ([cat, items]) => ` ${key(catLabel(cat))}${items.join(', ')}`,
+ );
+
+ const system = [
+ ` ${key('age')}—`,
+ profile.citizenship ? ` ${key('citizen')}${profile.citizenship}` : '',
+ ` ${key('datetime')}—`,
+ ].filter(Boolean);
+
+ return `${profile.name}
+ ${profile.bio}
+
+${title('contact')}
+${contact.join('\n')}
+
+${title('education')}
+${eduBlock.join('\n')}
+
+${title('experience')}
+${expBlock.join('\n')}
+
+${title('projects')}
+${projBlock.join('\n')}
+
+${title('skills')}
+${skillBlock.join('\n')}
+
+${title('system')}
+${system.join('\n')}
+
+type '${sfLink(
+ config.resume_url,
+ 'resume',
+ )}' or 'help' · 'ls' to browse sections`;
+};
+
+// --- summary: full-text profile (everything, flat) ---
+export const summary = async (_args: string[]): Promise => {
+ const b = (s: string) => ` • ${s}`;
+ const blocks: string[] = [];
+
+ blocks.push(`${profile.name}\n${profile.bio}`);
+
+ blocks.push(
+ [
+ title('EDUCATION'),
+ ...education.map((e) =>
+ [
+ `${e.school}`,
+ ...e.degrees.map((d) => ` ${d}`),
+ ` GPA ${e.gpa} · Graduation ${e.graduation}`,
+ ].join('\n'),
+ ),
+ ].join('\n'),
+ );
+
+ blocks.push(
+ [
+ title('EXPERIENCE'),
+ ...experience.map(
+ (e) =>
+ `${e.role} @ ${e.company} (${e.dates})\n${e.bullets
+ .map(b)
+ .join('\n')}`,
+ ),
+ ].join('\n\n'),
+ );
+
+ blocks.push(
+ [
+ title('PROJECTS'),
+ ...projects.map(
+ (p) => `${p.name} [${p.timeline}]\n${p.bullets.map(b).join('\n')}`,
+ ),
+ ].join('\n\n'),
+ );
+
+ blocks.push(
+ [
+ title('ORGANIZATIONS'),
+ ...organizations.map(
+ (o) =>
+ `${o.role} @ ${o.org} (${o.dates})\n${o.bullets.map(b).join('\n')}`,
+ ),
+ ].join('\n\n'),
+ );
+
+ blocks.push(
+ [
+ title('PUBLICATIONS'),
+ ...publications.map((p) => `${p.title}\n ${p.status}`),
+ ].join('\n\n'),
+ );
+
+ blocks.push(
+ [
+ title('SKILLS'),
+ ...Object.entries(skills).map(
+ ([cat, items]) =>
+ `${(catLabel(cat) + ':').padEnd(13)}${items.join(', ')}`,
+ ),
+ ].join('\n'),
+ );
+
+ return blocks.join('\n\n');
};
-export default sumfetch;
+export { sumfetch };
diff --git a/src/utils/resume.ts b/src/utils/resume.ts
new file mode 100644
index 0000000..c43b351
--- /dev/null
+++ b/src/utils/resume.ts
@@ -0,0 +1,65 @@
+// Single accessor for résumé data. resume.json is a generated mirror of
+// resume.org (see docs/resume-pipeline.org); nothing else should import the
+// JSON directly, so identity/content has exactly one in-code home.
+import resume from '../../resume.json';
+
+export type Profile = {
+ name: string;
+ github: string;
+ linkedin: string;
+ email: string;
+ phone?: string;
+ citizenship?: string;
+ bio: string;
+};
+export type Experience = {
+ id?: string;
+ role: string;
+ company: string;
+ dates: string;
+ bullets: string[];
+};
+export type Project = {
+ id?: string;
+ name: string;
+ timeline: string;
+ bullets: string[];
+};
+export type Education = {
+ id?: string;
+ school: string;
+ gpa: string;
+ graduation: string;
+ degrees: string[];
+ honors: string[];
+ coursework: string[];
+};
+export type Organization = {
+ id?: string;
+ role: string;
+ org: string;
+ dates: string;
+ bullets: string[];
+};
+export type Publication = {
+ id?: string;
+ title: string;
+ status: string;
+ bullets: string[];
+};
+export type Skills = Record;
+
+export const profile = resume.profile as Profile;
+export const experience = resume.experience as Experience[];
+export const projects = resume.projects as Project[];
+export const education = resume.education as Education[];
+export const organizations = resume.organizations as Organization[];
+export const publications = resume.publications as Publication[];
+export const skills = resume.skills as Skills;
+
+// Display label for a skills category key: acronyms (<=2 chars) upper-cased
+// whole ("ml" -> "ML"), everything else capitalized ("languages" -> "Languages").
+export const catLabel = (c: string) =>
+ c.length <= 2 ? c.toUpperCase() : c[0].toUpperCase() + c.slice(1);
+
+export default resume;
diff --git a/tailwind.config.js b/tailwind.config.js
index 2c63b9b..1025852 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,4 +1,10 @@
-const { colors } = require('./config.json');
+const config = require('./config.json');
+const themes = require('./themes.json');
+
+// Resolve colors from a named theme in themes.json when specified,
+// otherwise fall back to inline colors in config.json
+const colors =
+ (config.theme && themes[config.theme]) ? themes[config.theme] : config.colors;
module.exports = {
content: [
diff --git a/themes.json b/themes.json
index da2b297..e3e58d2 100644
--- a/themes.json
+++ b/themes.json
@@ -159,4 +159,25 @@
"red": "#EF6155"
}
}
+ ,
+ "matrix_crt": {
+ "light": {
+ "background": "#EAFBEA",
+ "foreground": "#103410",
+ "yellow": "#6B8E23",
+ "green": "#0A8F3B",
+ "gray": "#3A5F3A",
+ "blue": "#0E8D7A",
+ "red": "#B23A3A"
+ },
+ "dark": {
+ "background": "#0A0F0A",
+ "foreground": "#D2FFD2",
+ "yellow": "#9ACD32",
+ "green": "#00FF88",
+ "gray": "#2E3D2E",
+ "blue": "#00D4AA",
+ "red": "#FF3B3B"
+ }
+ }
}