1// src/lib/api.ts — dheerajsaxena.dev
2
3import { useState, useEffect, useCallback } from 'react'
4import type { FC, ReactNode } from 'react'
5
6const BASE = 'https://api.dheerajsaxena.dev/v1'
7const TIMEOUT = 8000
8
9interface ApiResponse<T> {
10 data: T
11 status: number
12 message: string
13}
14
15interface Project {
16 id: string
17 title: string
18 description: string
19 tech: string[]
20 liveUrl: string | null
21 githubUrl: string
22 featured: boolean
23 year: number
24}
25
26type Status = 'idle' | 'loading' | 'success' | 'error'
27
28async function apiFetch<T>(
29 path: string,
30 init?: RequestInit
31): Promise<T> {
32 const ac = new AbortController()
33 const timer = setTimeout(() => ac.abort(), TIMEOUT)
34 try {
35 const res = await fetch(BASE + path, {
36 headers: { 'Content-Type': 'application/json' },
37 signal: ac.signal,
38 ...init,
39 })
40 clearTimeout(timer)
41 if (!res.ok) {
42 throw new Error('HTTP ' + res.status)
43 }
44 const json: ApiResponse<T> = await res.json()
45 return json.data
46 } catch (err) {
47 clearTimeout(timer); throw err
48 }
49}
50
51export const getProjects = () =>
52 apiFetch<Project[]>('/projects')
53
54export function useAsync<T>(
55 fn: () => Promise<T>
56) {
57 const [data, setData] = useState<T | null>(null)
58 const [status, setStatus] = useState<Status>('idle')
59 const [error, setError] = useState<string | null>(null)
60
61 const run = useCallback(async () => {
62 setStatus('loading')
63 setError(null)
64 try {
65 const result = await fn()
66 setData(result)
67 setStatus('success')
68 } catch (e) {
69 setError((e as Error).message)
70 setStatus('error')
71 }
72 }, [])
73
74 useEffect(() => { run() }, [run])
75 return { data, status, error, refetch: run }
76}
77
78const Badge: FC<{ label: string }> = ({ label }) => (
79 <span className="badge">{label}</span>
80)
81
82function ProjectCard({
83 project,
84}: { project: Project }) {
85 const [hover, setHover] = useState(false)
86
87 return (
88 <article
89 className="project-card"
90 onMouseEnter={() => setHover(true)}
91 onMouseLeave={() => setHover(false)}
92 >
93 <div className="card-header">
94 <h3>{project.title}</h3>
95 {project.featured && (
96 <Badge label="Featured" />
97 )}
98 </div>
99 <p>{project.description}</p>
100 <ul className="tech-list">
101 {project.tech.map(tag => (
102 <li key={tag}><Badge label={tag} /></li>
103 ))}
104 </ul>
105 {hover && project.liveUrl && (
106 <a href={project.liveUrl} target="_blank"
107 rel="noreferrer">
108 Live Demo ↗
109 </a>
110 )}
111 </article>
112 )
113}
114
115export default function Portfolio() {
116 const {
117 data: projects,
118 status,
119 error,
120 } = useAsync(getProjects)
121
122 if (status === 'loading') {
123 return <div className="spinner" />
124 }
125
126 return (
127 <main className="portfolio">
128 <section id="hero">
129 <h1>Dheeraj Saxena</h1>
130 <p className="tagline">Full Stack Developer</p>
131 <nav>
132 <a href="#projects">View Work</a>
133 <a href="#contact">Contact</a>
134 </nav>
135 </section>
136 <section id="projects">
137 <div className="grid">
138 {(projects ?? []).map(p => (
139 <ProjectCard
140 key={p.id}
141 project={p}
142 />
143 ))}
144 </div>
145 </section>
146 <section id="contact">
147 <h2>Get In Touch</h2>
148 <a href="mailto:dheeraj@dheerajsaxena.dev">
149 dheeraj@dheerajsaxena.dev
150 </a>
151 </section>
152 </main>
153 )
154}
1// src/lib/api.ts — dheerajsaxena.dev
2
3import { useState, useEffect, useCallback } from 'react'
4import type { FC, ReactNode } from 'react'
5
6const BASE = 'https://api.dheerajsaxena.dev/v1'
7const TIMEOUT = 8000
8
9interface ApiResponse<T> {
10 data: T
11 status: number
12 message: string
13}
14
15interface Project {
16 id: string
17 title: string
18 description: string
19 tech: string[]
20 liveUrl: string | null
21 githubUrl: string
22 featured: boolean
23 year: number
24}
25
26type Status = 'idle' | 'loading' | 'success' | 'error'
27
28async function apiFetch<T>(
29 path: string,
30 init?: RequestInit
31): Promise<T> {
32 const ac = new AbortController()
33 const timer = setTimeout(() => ac.abort(), TIMEOUT)
34 try {
35 const res = await fetch(BASE + path, {
36 headers: { 'Content-Type': 'application/json' },
37 signal: ac.signal,
38 ...init,
39 })
40 clearTimeout(timer)
41 if (!res.ok) {
42 throw new Error('HTTP ' + res.status)
43 }
44 const json: ApiResponse<T> = await res.json()
45 return json.data
46 } catch (err) {
47 clearTimeout(timer); throw err
48 }
49}
50
51export const getProjects = () =>
52 apiFetch<Project[]>('/projects')
53
54export function useAsync<T>(
55 fn: () => Promise<T>
56) {
57 const [data, setData] = useState<T | null>(null)
58 const [status, setStatus] = useState<Status>('idle')
59 const [error, setError] = useState<string | null>(null)
60
61 const run = useCallback(async () => {
62 setStatus('loading')
63 setError(null)
64 try {
65 const result = await fn()
66 setData(result)
67 setStatus('success')
68 } catch (e) {
69 setError((e as Error).message)
70 setStatus('error')
71 }
72 }, [])
73
74 useEffect(() => { run() }, [run])
75 return { data, status, error, refetch: run }
76}
77
78const Badge: FC<{ label: string }> = ({ label }) => (
79 <span className="badge">{label}</span>
80)
81
82function ProjectCard({
83 project,
84}: { project: Project }) {
85 const [hover, setHover] = useState(false)
86
87 return (
88 <article
89 className="project-card"
90 onMouseEnter={() => setHover(true)}
91 onMouseLeave={() => setHover(false)}
92 >
93 <div className="card-header">
94 <h3>{project.title}</h3>
95 {project.featured && (
96 <Badge label="Featured" />
97 )}
98 </div>
99 <p>{project.description}</p>
100 <ul className="tech-list">
101 {project.tech.map(tag => (
102 <li key={tag}><Badge label={tag} /></li>
103 ))}
104 </ul>
105 {hover && project.liveUrl && (
106 <a href={project.liveUrl} target="_blank"
107 rel="noreferrer">
108 Live Demo ↗
109 </a>
110 )}
111 </article>
112 )
113}
114
115export default function Portfolio() {
116 const {
117 data: projects,
118 status,
119 error,
120 } = useAsync(getProjects)
121
122 if (status === 'loading') {
123 return <div className="spinner" />
124 }
125
126 return (
127 <main className="portfolio">
128 <section id="hero">
129 <h1>Dheeraj Saxena</h1>
130 <p className="tagline">Full Stack Developer</p>
131 <nav>
132 <a href="#projects">View Work</a>
133 <a href="#contact">Contact</a>
134 </nav>
135 </section>
136 <section id="projects">
137 <div className="grid">
138 {(projects ?? []).map(p => (
139 <ProjectCard
140 key={p.id}
141 project={p}
142 />
143 ))}
144 </div>
145 </section>
146 <section id="contact">
147 <h2>Get In Touch</h2>
148 <a href="mailto:dheeraj@dheerajsaxena.dev">
149 dheeraj@dheerajsaxena.dev
150 </a>
151 </section>
152 </main>
153 )
154}