Skip to content

Commit 2cfeef1

Browse files
feat: add an interesting last deployed info
1 parent fb999ac commit 2cfeef1

File tree

5 files changed

+368
-0
lines changed

5 files changed

+368
-0
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { NextResponse } from "next/server";
2+
3+
export async function GET() {
4+
try {
5+
// Fetch the latest deployment from Vercel API
6+
const vercelToken = process.env.VERCEL_TOKEN;
7+
const vercelProjectId = process.env.VERCEL_PROJECT_ID;
8+
9+
if (!vercelToken || !vercelProjectId) {
10+
throw new Error("Missing Vercel API configuration");
11+
}
12+
13+
const deploymentResponse = await fetch(
14+
`https://api.vercel.com/v6/deployments?&projectId=${vercelProjectId}&limit=1&state=READY`,
15+
{
16+
headers: {
17+
Authorization: `Bearer ${vercelToken}`,
18+
},
19+
}
20+
);
21+
22+
if (!deploymentResponse.ok) {
23+
throw new Error("Failed to fetch deployment data from Vercel");
24+
}
25+
26+
const deploymentData = await deploymentResponse.json();
27+
const latestDeployment = deploymentData.deployments[0];
28+
29+
// Fetch weather data from WeatherAPI.com API (free tier)
30+
const weatherApiKey = process.env.WEATHERAPI_KEY;
31+
const city = process.env.WEATHER_CITY || "Cuttack";
32+
let weatherData = null;
33+
34+
if (weatherApiKey) {
35+
const weatherResponse = await fetch(
36+
`https://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${city}&aqi=no`
37+
);
38+
39+
if (weatherResponse.ok) {
40+
const weatherJson = await weatherResponse.json();
41+
weatherData = {
42+
description: weatherJson.current.condition.text,
43+
temperature: weatherJson.current.temp_c,
44+
city: weatherJson.location.name,
45+
};
46+
}
47+
}
48+
49+
return NextResponse.json({
50+
deploymentInfo: latestDeployment
51+
? {
52+
createdAt: latestDeployment.created,
53+
url: latestDeployment.url,
54+
}
55+
: null,
56+
weatherInfo: weatherData,
57+
});
58+
} catch (error) {
59+
console.error("Error fetching status data:", error);
60+
return NextResponse.json(
61+
{ error: "Failed to fetch status data" },
62+
{ status: 500 }
63+
);
64+
}
65+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { NextResponse } from "next/server";
2+
3+
export async function GET() {
4+
try {
5+
const clientId = process.env.SPOTIFY_CLIENT_ID;
6+
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
7+
const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN;
8+
9+
if (!clientId || !clientSecret || !refreshToken) {
10+
throw new Error("Missing Spotify API configuration");
11+
}
12+
13+
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString(
14+
"base64"
15+
);
16+
17+
const response = await fetch("https://accounts.spotify.com/api/token", {
18+
method: "POST",
19+
headers: {
20+
Authorization: `Basic ${basicAuth}`,
21+
"Content-Type": "application/x-www-form-urlencoded",
22+
},
23+
body: new URLSearchParams({
24+
grant_type: "refresh_token",
25+
refresh_token: refreshToken,
26+
}),
27+
});
28+
29+
if (!response.ok) {
30+
throw new Error("Failed to refresh Spotify token");
31+
}
32+
33+
const data = await response.json();
34+
35+
return NextResponse.json({
36+
success: true,
37+
expiresIn: data.expires_in,
38+
});
39+
} catch (error) {
40+
console.error("Error refreshing Spotify token:", error);
41+
return NextResponse.json(
42+
{ error: "Failed to refresh Spotify token" },
43+
{ status: 500 }
44+
);
45+
}
46+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { NextResponse } from "next/server";
2+
3+
async function getSpotifyToken() {
4+
const clientId = process.env.SPOTIFY_CLIENT_ID;
5+
const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;
6+
const refreshToken = process.env.SPOTIFY_REFRESH_TOKEN;
7+
8+
if (!clientId || !clientSecret || !refreshToken) {
9+
throw new Error("Missing Spotify credentials");
10+
}
11+
12+
const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString(
13+
"base64"
14+
);
15+
16+
const response = await fetch("https://accounts.spotify.com/api/token", {
17+
method: "POST",
18+
headers: {
19+
Authorization: `Basic ${basicAuth}`,
20+
"Content-Type": "application/x-www-form-urlencoded",
21+
},
22+
body: new URLSearchParams({
23+
grant_type: "refresh_token",
24+
refresh_token: refreshToken,
25+
}),
26+
});
27+
28+
if (!response.ok) {
29+
throw new Error("Failed to refresh token");
30+
}
31+
32+
const data = await response.json();
33+
return data.access_token;
34+
}
35+
36+
export async function GET() {
37+
try {
38+
// Get a fresh token on each request
39+
const accessToken = await getSpotifyToken();
40+
41+
// Use the fresh token to call Spotify API
42+
const spotifyResponse = await fetch(
43+
"https://api.spotify.com/v1/me/player/currently-playing",
44+
{
45+
headers: {
46+
Authorization: `Bearer ${accessToken}`,
47+
},
48+
}
49+
);
50+
51+
// If status is 204, nothing is playing
52+
if (spotifyResponse.status === 204) {
53+
return NextResponse.json({ isPlaying: false });
54+
}
55+
56+
if (!spotifyResponse.ok) {
57+
throw new Error(`Spotify API error: ${spotifyResponse.status}`);
58+
}
59+
60+
const songData = await spotifyResponse.json();
61+
62+
return NextResponse.json({
63+
isPlaying: songData.is_playing,
64+
songInfo: songData.item
65+
? {
66+
name: songData.item.name,
67+
artist: songData.item.artists.map((a: any) => a.name).join(", "),
68+
url: songData.item.external_urls.spotify,
69+
albumArt: songData.item.album.images[0]?.url,
70+
}
71+
: null,
72+
});
73+
} catch (error) {
74+
console.error("Error fetching Spotify data:", error);
75+
return NextResponse.json({ isPlaying: false, error: String(error) });
76+
}
77+
}

src/app/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
InteractiveTerminal,
2020
MatrixRain,
2121
} from "@/components/InteractiveElements";
22+
import LastDeployedInfo from "@/components/LastDeployedInfo";
2223

2324
const companies = [
2425
{
@@ -507,6 +508,8 @@ const HeroSection = () => (
507508
no state of "completeness" to a website.
508509
</a>
509510
</p>
511+
512+
<LastDeployedInfo />
510513
</div>
511514

512515
<motion.div
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
"use client";
2+
3+
import React, { useEffect, useState } from "react";
4+
import { format } from "date-fns";
5+
import {
6+
Calendar,
7+
Headphones,
8+
CloudRain,
9+
CloudSnow,
10+
CloudLightning,
11+
CloudDrizzle,
12+
Cloud,
13+
Sun,
14+
} from "lucide-react";
15+
16+
interface DeploymentInfo {
17+
createdAt: string;
18+
url: string;
19+
}
20+
21+
interface SongInfo {
22+
name: string;
23+
artist: string;
24+
url: string;
25+
}
26+
27+
interface WeatherInfo {
28+
description: string;
29+
temperature: number;
30+
city: string;
31+
}
32+
33+
export default function DeploymentStatus() {
34+
const [deploymentInfo, setDeploymentInfo] = useState<DeploymentInfo | null>(
35+
null
36+
);
37+
const [songInfo, setSongInfo] = useState<SongInfo | null>(null);
38+
const [weatherInfo, setWeatherInfo] = useState<WeatherInfo | null>(null);
39+
const [isLoading, setIsLoading] = useState(true);
40+
41+
useEffect(() => {
42+
const fetchData = async () => {
43+
try {
44+
setIsLoading(true);
45+
46+
// Fetch deployment status
47+
const deploymentResponse = await fetch("/api/deployment-status");
48+
if (deploymentResponse.ok) {
49+
const data = await deploymentResponse.json();
50+
setDeploymentInfo(data.deploymentInfo);
51+
setWeatherInfo(data.weatherInfo);
52+
}
53+
54+
// Fetch Spotify separately
55+
const spotifyResponse = await fetch("/api/spotify/now-playing");
56+
if (spotifyResponse.ok) {
57+
const data = await spotifyResponse.json();
58+
if (data.isPlaying && data.songInfo) {
59+
setSongInfo(data.songInfo);
60+
}
61+
}
62+
} catch (err) {
63+
console.error("Error fetching status data:", err);
64+
} finally {
65+
setIsLoading(false);
66+
}
67+
};
68+
69+
fetchData();
70+
}, []);
71+
72+
if (isLoading) {
73+
return (
74+
<div className="text-sm text-gray-500 dark:text-gray-400 mt-4 animate-pulse">
75+
Loading latest site info...
76+
</div>
77+
);
78+
}
79+
80+
if (!deploymentInfo) {
81+
return null; // Don't show anything if there's no deployment info
82+
}
83+
84+
// Format the deployment date
85+
const formattedDate = format(
86+
new Date(deploymentInfo.createdAt),
87+
"MMMM d, yyyy"
88+
);
89+
const formattedTime = format(new Date(deploymentInfo.createdAt), "h:mm a");
90+
91+
// Get the appropriate weather icon based on the description
92+
const getWeatherIcon = (description: string) => {
93+
const desc = description.toLowerCase();
94+
if (desc.includes("rain") || desc.includes("shower"))
95+
return (
96+
<CloudRain size={16} className="inline-block ml-1 text-blue-500" />
97+
);
98+
if (desc.includes("snow"))
99+
return (
100+
<CloudSnow size={16} className="inline-block ml-1 text-blue-200" />
101+
);
102+
if (desc.includes("thunder") || desc.includes("lightning"))
103+
return (
104+
<CloudLightning
105+
size={16}
106+
className="inline-block ml-1 text-yellow-500"
107+
/>
108+
);
109+
if (desc.includes("drizzl"))
110+
return (
111+
<CloudDrizzle size={16} className="inline-block ml-1 text-blue-400" />
112+
);
113+
if (desc.includes("cloud") || desc.includes("overcast"))
114+
return <Cloud size={16} className="inline-block ml-1 text-gray-400" />;
115+
if (desc.includes("mist") || desc.includes("fog"))
116+
return <Cloud size={16} className="inline-block ml-1 text-gray-300" />;
117+
if (desc.includes("clear") || desc.includes("sunny"))
118+
return <Sun size={16} className="inline-block ml-1 text-yellow-500" />;
119+
return <Cloud size={16} className="inline-block ml-1 text-gray-400" />;
120+
};
121+
122+
// Get a more relatable weather description
123+
const getWeatherDescription = (description: string) => {
124+
const desc = description.toLowerCase();
125+
if (desc.includes("mist") || desc.includes("fog")) return "foggy";
126+
if (desc.includes("rain") && desc.includes("heavy")) return "pouring";
127+
if (desc.includes("rain")) return "rainy";
128+
if (desc.includes("drizzl")) return "drizzling";
129+
if (desc.includes("snow")) return "snowing";
130+
if (desc.includes("thunder")) return "stormy";
131+
if (desc.includes("cloud") || desc.includes("overcast")) return "cloudy";
132+
if (desc.includes("clear") || desc.includes("sunny")) return "sunny";
133+
return description.toLowerCase();
134+
};
135+
136+
return (
137+
<div className="text-sm text-gray-600 dark:text-gray-300 mt-4 leading-relaxed italic border-t border-gray-200 dark:border-gray-700 pt-4">
138+
<div className="flex items-start">
139+
<Calendar className="w-4 h-4 mt-0.5 text-green-600 dark:text-green-400 flex-shrink-0 mr-2" />
140+
<span>
141+
This site was last deployed on {formattedDate} at {formattedTime}
142+
{songInfo && (
143+
<span>
144+
{" while listening to "}
145+
<a
146+
href={songInfo.url}
147+
target="_blank"
148+
rel="noopener noreferrer"
149+
className="text-green-600 dark:text-green-400 hover:underline font-medium"
150+
>
151+
{songInfo.name}
152+
</a>{" "}
153+
<span className="whitespace-nowrap">
154+
by {songInfo.artist}{" "}
155+
<Headphones size={14} className="inline-block -mt-0.5" />
156+
</span>
157+
</span>
158+
)}
159+
{weatherInfo && (
160+
<span>
161+
{songInfo ? "." : ","} The weather was{" "}
162+
{getWeatherDescription(weatherInfo.description)}
163+
{getWeatherIcon(weatherInfo.description)}
164+
{" at "}
165+
<span className="font-medium">
166+
{Math.round(weatherInfo.temperature)}°C
167+
</span>
168+
{" in "}
169+
<span className="font-medium">{weatherInfo.city}</span>
170+
{"."}
171+
</span>
172+
)}
173+
</span>
174+
</div>
175+
</div>
176+
);
177+
}

0 commit comments

Comments
 (0)