source-code/
portofolio-neo-gruv
Public
codeCodeinfoIssues 0call_splitPull Requestsplay_circleActions
portofolio-neo-gruv/src/services/api.ts
typescript139 lines4.5 KB
import { 
    SiteData, 
    HeroData, 
    AboutData, 
    ContactData, 
    NavigationData, 
    FooterData, 
    ResumeData, 
    SkillItem 
} from '../types';
import { 
    mapProject, 
    mapExperience, 
    ProjectRaw, 
    ExperienceRaw 
} from './mappers';

/**
 * Custom error class for API failures.
 * Captures the HTTP status code and response details.
 */
export class APIError extends Error {
    constructor(public status: number, message: string) {
        super(message);
        this.name = 'APIError';
    }
}

/**
 * Generic fetcher with error handling.
 * Integrates Next.js Incremental Static Regeneration (ISR) configuration via next.revalidate.
 * 
 * @param url - The full API URL endpoint
 * @param revalidate - Cache lifetime in seconds (defaults to 3600 / 1 hour)
 */
async function fetchWithError<T>(url: string, revalidate: number = 3600): Promise<T> {
    const response = await fetch(url, {
        next: { revalidate } // ISR: Cache for N seconds, then revalidate in the background
    });
    if (!response.ok) {
        throw new APIError(response.status, `Failed to fetch from ${url}: ${response.statusText}`);
    }
    return await response.json() as T;
}

// ---- INDIVIDUAL DOMAIN FETCHERS (SRP) ----

export async function fetchHero(baseUrl: string, revalidate = 3600): Promise<HeroData> {
    return fetchWithError<HeroData>(`${baseUrl}/config/hero`, revalidate);
}

export async function fetchAbout(baseUrl: string, revalidate = 3600): Promise<AboutData> {
    return fetchWithError<AboutData>(`${baseUrl}/config/about`, revalidate);
}

export async function fetchContact(baseUrl: string, revalidate = 3600): Promise<ContactData> {
    return fetchWithError<ContactData>(`${baseUrl}/config/contact`, revalidate);
}

export async function fetchMarquee(baseUrl: string, revalidate = 3600): Promise<string[]> {
    return fetchWithError<string[]>(`${baseUrl}/config/marquee`, revalidate);
}

export async function fetchNavigation(baseUrl: string, revalidate = 86400): Promise<NavigationData> {
    return fetchWithError<NavigationData>(`${baseUrl}/config/navigation`, revalidate);
}

export async function fetchFooter(baseUrl: string, revalidate = 86400): Promise<FooterData> {
    return fetchWithError<FooterData>(`${baseUrl}/config/footer`, revalidate);
}

export async function fetchResume(baseUrl: string, revalidate = 3600): Promise<ResumeData> {
    return fetchWithError<ResumeData>(`${baseUrl}/config/resume`, revalidate);
}

export async function fetchProjects(baseUrl: string, revalidate = 1800): Promise<ProjectRaw[]> {
    return fetchWithError<ProjectRaw[]>(`${baseUrl}/portfolio/projects`, revalidate);
}

export async function fetchExperiences(baseUrl: string, revalidate = 3600): Promise<ExperienceRaw[]> {
    return fetchWithError<ExperienceRaw[]>(`${baseUrl}/portfolio/experiences`, revalidate);
}

export async function fetchSkills(baseUrl: string, revalidate = 3600): Promise<SkillItem[]> {
    return fetchWithError<SkillItem[]>(`${baseUrl}/portfolio/skills`, revalidate);
}

/**
 * Orchestrator to fetch all site configurations and portfolio data concurrently.
 * Employs Promise.all for optimized HTTP request parallelism.
 * Transforms API response structures into frontend domain models using mappers (SRP).
 * 
 * @param baseUrl - The backend domain base URL
 * @returns Fully compiled SiteData object for the portfolio page
 */
export async function fetchSiteData(baseUrl: string): Promise<SiteData> {
    // Run all HTTP requests in parallel to avoid waterfalls
    const [
        heroData,
        aboutData,
        contactData,
        marqueeItems,
        navigationData,
        footerData,
        resumeData,
        projectsDataRaw,
        experiencesDataRaw,
        skillsData
    ] = await Promise.all([
        fetchHero(baseUrl),
        fetchAbout(baseUrl),
        fetchContact(baseUrl),
        fetchMarquee(baseUrl),
        fetchNavigation(baseUrl),
        fetchFooter(baseUrl),
        fetchResume(baseUrl),
        fetchProjects(baseUrl),
        fetchExperiences(baseUrl),
        fetchSkills(baseUrl),
    ]);

    // Map database/API entities to localized frontend models
    const mappedProjects = projectsDataRaw.map(mapProject);
    const mappedExperiences = experiencesDataRaw.map(mapExperience);

    return {
        heroData,
        aboutData,
        contactData,
        marqueeItems,
        navigationData,
        footerData,
        resumeData,
        projectsData: mappedProjects,
        experiencesData: mappedExperiences,
        skillsData
    };
}

About

Custom portfolio frontend designed using retro Neo-Brutalist styling. Features server-rendered pages, persistent codebase layout, interactive file explorer tree, and Shiki code syntax highlighting.

TypeScriptNext.jsReact 19Tailwind CSSShiki

Contributors

1