Fixed spotify
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m26s

This commit is contained in:
2025-06-19 23:34:44 -06:00
parent 7161584dcd
commit 41614a49a8
8 changed files with 413 additions and 282 deletions

41
src/utils/spotify.ts Normal file
View File

@@ -0,0 +1,41 @@
interface SpotifyCredentials {
clientId: string;
clientSecret: string;
refreshToken: string;
accessToken?: string;
}
/**
* Get Spotify credentials from environment variables
* Checks both process.env and import.meta.env for compatibility
*/
export function getSpotifyCredentials(): SpotifyCredentials | null {
const clientId =
process.env.SPOTIFY_CLIENT_ID || import.meta.env.SPOTIFY_CLIENT_ID;
const clientSecret =
process.env.SPOTIFY_CLIENT_SECRET ||
import.meta.env.SPOTIFY_CLIENT_SECRET;
const refreshToken =
process.env.SPOTIFY_REFRESH_TOKEN ||
import.meta.env.SPOTIFY_REFRESH_TOKEN;
const accessToken =
process.env.SPOTIFY_ACCESS_TOKEN || import.meta.env.SPOTIFY_ACCESS_TOKEN;
if (!clientId || !clientSecret || !refreshToken) {
return null;
}
return {
clientId,
clientSecret,
refreshToken,
accessToken,
};
}
/**
* Check if Spotify integration is properly configured
*/
export function isSpotifyConfigured(): boolean {
return getSpotifyCredentials() !== null;
}

View File

@@ -0,0 +1,250 @@
import type { FileSystemNode } from "./types";
import { getCurrentDirectory, resolvePath } from "./fs";
export interface CommandContext {
currentPath: string;
fileSystem: { [key: string]: FileSystemNode };
setCurrentPath: (path: string) => void;
setIsTrainRunning: (running: boolean) => void;
setTrainPosition: (position: number) => void;
}
export function executeCommand(input: string, context: CommandContext): string {
const trimmedInput = input.trim();
if (!trimmedInput) return "";
const [command, ...args] = trimmedInput.split(" ");
switch (command.toLowerCase()) {
case "help":
return handleHelp();
case "ls":
return handleLs(args, context);
case "cd":
return handleCd(args, context);
case "pwd":
return handlePwd(context);
case "cat":
return handleCat(args, context);
case "tree":
return handleTree(context);
case "clear":
return "";
case "whoami":
return handleWhoami();
case "open":
return handleOpen(args);
case "sl":
return handleSl(context);
default:
return `${command}: command not found. Type 'help' for available commands.`;
}
}
function handleHelp(): string {
return `Available commands:
ls [path] - List directory contents
cd <path> - Change directory
cat <file> - Display file contents
pwd - Show current directory
clear - Clear terminal
tree - Show directory structure
whoami - Display user info
open <path> - Open page in browser (simulated)
help - Show this help message
Navigation:
Use "cd .." to go up one directory
Use "cd /" to go to root directory
File paths can be relative or absolute
Use TAB for auto-completion
Examples:
ls
cd resume
cat about.txt
open /resume
open /talks
cat /talks/README.txt
cd social
cat /tech/README.txt`;
}
function handleLs(args: string[], context: CommandContext): string {
const { currentPath, fileSystem } = context;
const targetPath = args[0] ? resolvePath(currentPath, args[0]) : currentPath;
const pathParts = targetPath.split("/").filter((part: string) => part !== "");
let target = fileSystem["/"];
for (const part of pathParts) {
if (
target?.children &&
target.children[part] &&
target.children[part].type === "directory"
) {
target = target.children[part];
} else if (pathParts.length > 0) {
return `ls: cannot access '${targetPath}': No such file or directory`;
}
}
if (!target?.children) {
return `ls: cannot access '${targetPath}': Not a directory`;
}
const items = Object.values(target.children)
.map((item) => {
const color = item.type === "directory" ? "\x1b[34m" : "\x1b[0m";
const suffix = item.type === "directory" ? "/" : "";
return `${color}${item.name}${suffix}\x1b[0m`;
})
.join(" ");
return items || "Directory is empty";
}
function handleCd(args: string[], context: CommandContext): string {
const { currentPath, fileSystem, setCurrentPath } = context;
const targetPath = args[0] ? resolvePath(currentPath, args[0]) : "/";
const pathParts = targetPath.split("/").filter((part: string) => part !== "");
let current = fileSystem["/"];
for (const part of pathParts) {
if (
current?.children &&
current.children[part] &&
current.children[part].type === "directory"
) {
current = current.children[part];
} else {
return `cd: no such file or directory: ${targetPath}`;
}
}
setCurrentPath(targetPath || "/");
return "";
}
function handlePwd(context: CommandContext): string {
return context.currentPath || "/";
}
function handleCat(args: string[], context: CommandContext): string {
const { currentPath, fileSystem } = context;
if (!args[0]) {
return "cat: missing file argument";
}
const filePath = resolvePath(currentPath, args[0]);
const pathParts = filePath.split("/").filter((part: string) => part !== "");
const fileName = pathParts.pop();
let current = fileSystem["/"];
for (const part of pathParts) {
if (
current?.children &&
current.children[part] &&
current.children[part].type === "directory"
) {
current = current.children[part];
} else {
return `cat: ${filePath}: No such file or directory`;
}
}
if (!fileName || !current?.children || !current.children[fileName]) {
return `cat: ${filePath}: No such file or directory`;
}
const file = current.children[fileName];
if (file.type !== "file") {
return `cat: ${filePath}: Is a directory`;
}
return file.content || "";
}
function handleTree(context: CommandContext): string {
const { fileSystem } = context;
const buildTree = (
node: FileSystemNode,
prefix: string = "",
isLast: boolean = true,
): string => {
let result = "";
if (!node.children) return result;
const entries = Object.entries(node.children);
entries.forEach(([name, child], index) => {
const isLastChild = index === entries.length - 1;
const connector = isLastChild ? "└── " : "├── ";
const color = child.type === "directory" ? "\x1b[34m" : "\x1b[0m";
const suffix = child.type === "directory" ? "/" : "";
result += `${prefix}${connector}${color}${name}${suffix}\x1b[0m\n`;
if (child.type === "directory") {
const newPrefix = prefix + (isLastChild ? " " : "│ ");
result += buildTree(child, newPrefix, isLastChild);
}
});
return result;
};
return ".\n" + buildTree(fileSystem["/"]);
}
function handleWhoami(): string {
return "guest@atri.dad";
}
function handleOpen(args: string[]): string {
const path = args[0];
if (!path) {
return "open: missing path argument";
}
let url = "";
if (path === "/resume" || path.startsWith("/resume")) {
url = "/resume";
} else if (path === "/projects" || path.startsWith("/projects")) {
url = "/projects";
} else if (path === "/posts" || path.startsWith("/posts")) {
url = "/posts";
} else if (path === "/talks" || path.startsWith("/talks")) {
url = "/talks";
} else if (path === "/" || path === "/about.txt") {
url = "/";
} else {
return `open: cannot open '${path}': No associated page`;
}
window.open(url, "_blank");
return `Opening ${url} in new tab...`;
}
function handleSl(context: CommandContext): string {
const { setIsTrainRunning, setTrainPosition } = context;
setIsTrainRunning(true);
setTrainPosition(100);
const animateTrain = () => {
let position = 100;
const interval = setInterval(() => {
position -= 1.5;
setTrainPosition(position);
if (position < -50) {
clearInterval(interval);
setIsTrainRunning(false);
}
}, 60);
};
setTimeout(animateTrain, 100);
return "";
}

303
src/utils/terminal/fs.ts Normal file
View File

@@ -0,0 +1,303 @@
import type { FileSystemNode, ResumeData } from './types';
import { talks, projects, socialLinks, techLinks } from '../../config/data';
export async function buildFileSystem(): Promise<{ [key: string]: FileSystemNode }> {
try {
const response = await fetch('/files/resume.json');
const resumeData: ResumeData = await response.json();
// Fetch blog posts
const postsResponse = await fetch('/api/posts.json');
let postsData = [];
try {
postsData = await postsResponse.json();
} catch (error) {
console.log('Could not fetch posts data:', error);
}
// Build resume files from rxresume json
const resumeFiles = buildResumeFiles(resumeData);
const postsFiles = buildPostsFiles(postsData);
const talksFiles = buildTalksFiles();
const projectsFiles = buildProjectsFiles();
const socialFiles = buildSocialFiles();
const techFiles = buildTechFiles();
const contactContent = buildContactContent(resumeData);
const fs: { [key: string]: FileSystemNode } = {
'/': {
type: 'directory',
name: '/',
children: {
'about.txt': {
type: 'file',
name: 'about.txt',
content: `${resumeData.basics.name}\nResearcher, Full-Stack Developer, and IT Professional.\n\nExplore the directories:\n- /resume - Professional experience and skills\n- /posts - Blog posts and articles\n- /talks - Conference presentations\n- /projects - Personal and professional projects\n- /social - Social media and contact links\n- /tech - Technologies and tools I use\n\nType "ls" to see all available files and directories.`
},
'resume': {
type: 'directory',
name: 'resume',
children: resumeFiles
},
'posts': {
type: 'directory',
name: 'posts',
children: postsFiles
},
'talks': {
type: 'directory',
name: 'talks',
children: talksFiles
},
'projects': {
type: 'directory',
name: 'projects',
children: projectsFiles
},
'social': {
type: 'directory',
name: 'social',
children: socialFiles
},
'tech': {
type: 'directory',
name: 'tech',
children: techFiles
},
'contact.txt': {
type: 'file',
name: 'contact.txt',
content: contactContent
}
}
}
};
return fs;
} catch (error) {
console.error('Error loading resume data:', error);
return buildFallbackFileSystem();
}
}
function buildResumeFiles(resumeData: ResumeData): { [key: string]: FileSystemNode } {
const resumeFiles: { [key: string]: FileSystemNode } = {};
if (resumeData.sections.summary) {
resumeFiles['summary.txt'] = {
type: 'file',
name: 'summary.txt',
content: resumeData.sections.summary.content.replace(/<[^>]*>/g, '')
};
}
if (resumeData.sections.skills?.items) {
const skillsContent = resumeData.sections.skills.items
.map(skill => `${skill.name} (Level: ${skill.level}/5)`)
.join('\n');
resumeFiles['skills.txt'] = {
type: 'file',
name: 'skills.txt',
content: skillsContent
};
}
if (resumeData.sections.experience?.items) {
const experienceContent = resumeData.sections.experience.items
.map(exp => {
const summary = exp.summary.replace(/<[^>]*>/g, '').replace(/&nbsp;/g, ' ');
return `${exp.position} at ${exp.company}\n${exp.date} | ${exp.location}\n${summary}\n${exp.url?.href ? `URL: ${exp.url.href}` : ''}\n`;
})
.join('\n---\n\n');
resumeFiles['experience.txt'] = {
type: 'file',
name: 'experience.txt',
content: experienceContent
};
}
if (resumeData.sections.education?.items) {
const educationContent = resumeData.sections.education.items
.map(edu => `${edu.institution}\n${edu.studyType} - ${edu.area}\n${edu.date}\n${edu.summary ? edu.summary.replace(/<[^>]*>/g, '') : ''}`)
.join('\n\n---\n\n');
resumeFiles['education.txt'] = {
type: 'file',
name: 'education.txt',
content: educationContent
};
}
if (resumeData.sections.volunteer?.items) {
const volunteerContent = resumeData.sections.volunteer.items
.map(vol => `${vol.organization}\n${vol.position}\n${vol.date}`)
.join('\n\n---\n\n');
resumeFiles['volunteer.txt'] = {
type: 'file',
name: 'volunteer.txt',
content: volunteerContent
};
}
return resumeFiles;
}
function buildPostsFiles(postsData: any[]): { [key: string]: FileSystemNode } {
const postsFiles: { [key: string]: FileSystemNode } = {};
postsData.forEach((post: any) => {
const fileName = `${post.slug}.md`;
let content = `---
title: "${post.title}"
description: "${post.description}"
pubDate: "${post.pubDate}"
tags: [${post.tags.map((tag: string) => `"${tag}"`).join(', ')}]
---
${post.content}`;
postsFiles[fileName] = {
type: 'file',
name: fileName,
content
};
});
return postsFiles;
}
function buildTalksFiles(): { [key: string]: FileSystemNode } {
const talksFiles: { [key: string]: FileSystemNode } = {};
talks.forEach(talk => {
const fileName = `${talk.id}.txt`;
let content = `${talk.name}
${talk.description}
${talk.venue || ''}
${talk.date || ''}
${talk.link}`;
talksFiles[fileName] = {
type: 'file',
name: fileName,
content
};
});
return talksFiles;
}
function buildProjectsFiles(): { [key: string]: FileSystemNode } {
const projectsFiles: { [key: string]: FileSystemNode } = {};
projects.forEach(project => {
const fileName = `${project.id}.txt`;
let content = `${project.name}
${project.description}
${project.status || ''}
${project.technologies ? project.technologies.join(', ') : ''}
${project.link}`;
projectsFiles[fileName] = {
type: 'file',
name: fileName,
content
};
});
return projectsFiles;
}
function buildSocialFiles(): { [key: string]: FileSystemNode } {
const socialFiles: { [key: string]: FileSystemNode } = {};
socialLinks.forEach(link => {
const fileName = `${link.id}.txt`;
let content = `${link.name}
${link.url}`;
socialFiles[fileName] = {
type: 'file',
name: fileName,
content
};
});
return socialFiles;
}
function buildTechFiles(): { [key: string]: FileSystemNode } {
const techFiles: { [key: string]: FileSystemNode } = {};
techLinks.forEach(link => {
const fileName = `${link.id}.txt`;
let content = `${link.name}
${link.url}`;
techFiles[fileName] = {
type: 'file',
name: fileName,
content
};
});
return techFiles;
}
function buildContactContent(resumeData: ResumeData): string {
return [
`Email: ${resumeData.basics.email}`,
'',
'Social Profiles:',
...resumeData.sections.profiles.items.map(profile =>
`${profile.network}: ${profile.url.href}`
)
].join('\n');
}
function buildFallbackFileSystem(): { [key: string]: FileSystemNode } {
return {
'/': {
type: 'directory',
name: '/',
children: {
'about.txt': {
type: 'file',
name: 'about.txt',
content: 'Atridad Lahiji\nResearcher, Full-Stack Developer, and IT Professional.\n\nError loading detailed information. Please check the website directly.'
}
}
}
};
}
export function getCurrentDirectory(fileSystem: { [key: string]: FileSystemNode }, currentPath: string): FileSystemNode {
const pathParts = currentPath.split('/').filter((part: string) => part !== '');
let current = fileSystem['/'];
for (const part of pathParts) {
if (current?.children && current.children[part] && current.children[part].type === 'directory') {
current = current.children[part];
}
}
return current;
}
export function resolvePath(currentPath: string, path: string): string {
if (path.startsWith('/')) {
return path;
}
const currentParts = currentPath.split('/').filter((part: string) => part !== '');
const pathParts = path.split('/');
for (const part of pathParts) {
if (part === '..') {
currentParts.pop();
} else if (part !== '.' && part !== '') {
currentParts.push(part);
}
}
return '/' + currentParts.join('/');
}

View File

@@ -0,0 +1,40 @@
export interface FileSystemNode {
type: 'directory' | 'file';
name: string;
content?: string;
children?: { [key: string]: FileSystemNode };
}
export interface ResumeData {
basics: {
name: string;
email: string;
url?: { href: string };
};
sections: {
summary: { name: string; content: string };
profiles: { name: string; items: { network: string; username: string; url: { href: string } }[] };
skills: { name: string; items: { id: string; name: string; level: number }[] };
experience: { name: string; items: { id: string; company: string; position: string; date: string; location: string; summary: string; url?: { href: string } }[] };
education: { name: string; items: { id: string; institution: string; studyType: string; area: string; date: string; summary: string }[] };
volunteer: { name: string; items: { id: string; organization: string; position: string; date: string }[] };
};
}
export interface Command {
input: string;
output: string;
timestamp: Date;
path: string;
}
export interface TerminalState {
currentPath: string;
commandHistory: Command[];
currentInput: string;
historyIndex: number;
fileSystem: { [key: string]: FileSystemNode };
isTrainRunning: boolean;
trainPosition: number;
persistentHistory: string[];
}

108
src/utils/terminal/utils.ts Normal file
View File

@@ -0,0 +1,108 @@
import type { FileSystemNode } from "./types";
import { resolvePath } from "./fs";
export function getCompletions(
input: string,
currentPath: string,
fileSystem: { [key: string]: FileSystemNode },
): { completion: string | null; replaceFrom: number } {
const parts = input.trim().split(" ");
const command = parts[0];
const partialPath = parts[parts.length - 1] || "";
// Only complete paths for these commands
if (parts.length > 1 && ["ls", "cd", "cat", "open"].includes(command)) {
// Path completion
const isAbsolute = partialPath.startsWith("/");
const pathToComplete = isAbsolute
? partialPath
: resolvePath(currentPath, partialPath);
// Find the directory to search in and the prefix to match
let dirPath: string;
let searchPrefix: string;
if (pathToComplete.endsWith("/")) {
// Path ends with slash - complete inside this directory
dirPath = pathToComplete;
searchPrefix = "";
} else {
// Base case - find directory and prefix
const lastSlash = pathToComplete.lastIndexOf("/");
if (lastSlash >= 0) {
dirPath = pathToComplete.substring(0, lastSlash + 1);
searchPrefix = pathToComplete.substring(lastSlash + 1);
} else {
dirPath = currentPath.endsWith("/") ? currentPath : currentPath + "/";
searchPrefix = pathToComplete;
}
}
// Calculate where to start replacement in the original input
const spaceBeforeArg = input.lastIndexOf(" ");
const replaceFrom = spaceBeforeArg >= 0 ? spaceBeforeArg + 1 : 0;
// Navigate to the directory
const dirParts = dirPath.split("/").filter((part: string) => part !== "");
let current = fileSystem["/"];
for (const part of dirParts) {
if (
current?.children &&
current.children[part] &&
current.children[part].type === "directory"
) {
current = current.children[part];
} else {
return { completion: null, replaceFrom };
}
}
if (!current?.children) {
return { completion: null, replaceFrom };
}
// Get first matching item
const match = Object.keys(current.children).find((name) =>
name.startsWith(searchPrefix),
);
if (match) {
const item = current.children[match];
const completion = item.type === "directory" ? `${match}/` : match;
return { completion, replaceFrom };
}
}
return { completion: null, replaceFrom: input.length };
}
export function formatOutput(text: string): string {
return text
.replace(/\x1b\[34m/g, '<span class="text-primary">')
.replace(/\x1b\[0m/g, "</span>");
}
export function saveCommandToHistory(
command: string,
persistentHistory: string[],
): string[] {
if (command.trim()) {
const updatedHistory = [...persistentHistory, command].slice(-100); // Keep last 100 commands
localStorage.setItem("terminal-history", JSON.stringify(updatedHistory));
return updatedHistory;
}
return persistentHistory;
}
export function loadCommandHistory(): string[] {
const savedHistory = localStorage.getItem("terminal-history");
if (savedHistory) {
try {
return JSON.parse(savedHistory);
} catch (error) {
console.error("Error loading command history:", error);
}
}
return [];
}