Skip to content

Commit

Permalink
add homepage
Browse files Browse the repository at this point in the history
  • Loading branch information
Vahor committed Apr 30, 2024
1 parent 8cc245a commit 22a3b97
Show file tree
Hide file tree
Showing 14 changed files with 296 additions and 17 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ next-env.d.ts

# local
.envrc
*.local
Empty file added public/currently-listening.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/spotify.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/tmp.jpg
Binary file not shown.
18 changes: 18 additions & 0 deletions scripts/spotify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generate access code

scope="user-top-read"
redirect_uri="https://vahor.fr"

echo "Using client_id: $SPOTIFY_CLIENT_ID"

url="https://accounts.spotify.com/authorize?client_id=$SPOTIFY_CLIENT_ID&response_type=code&redirect_uri=$redirect_uri&scope=$scope"

echo "Go to this URL: $url"
echo "Paste the code here:"
read code

echo "Using code: $code"

res=$(curl -s -d client_id=$SPOTIFY_CLIENT_ID -d client_secret=$SPOTIFY_CLIENT_SECRET -d grant_type=authorization_code -d code=$code -d redirect_uri=$redirect_uri https://accounts.spotify.com/api/token)

echo $res | jq
81 changes: 70 additions & 11 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,79 @@
import A from "@/components/A";
import { CurrentlyListeningSvg } from "@/components/CurrentlyListeningSvg";
import { SpotifyTopTrackBadge } from "@/components/SpotifyTopArtist";
import { UrlBadge } from "@/components/UrlBadge";
import { JsonLd } from "@/components/jsonld/profile-page";
import { GITHUB_PROFILE, TWITTER_PROFILE } from "@/lib/constants";
import { author } from "@/lib/jsonld";
import { allPosts } from "contentlayer/generated";
import Link from "next/link";

const postCount = allPosts.length;

export default function Home() {
return (
<div>
<main className="py-16 mx-auto container post-content">
<JsonLd jsonLd={author} />
<ul>
{allPosts.map((post) => (
<li key={post._raw.flattenedPath}>
<h2>{post.title}</h2>
<Link href={post.url}>{post.url}</Link>
</li>
))}
</ul>
</div>
<h1 className="text-2xl text-black dark:text-white font-semibold mb-8">
Nathan David
</h1>
<section>
<p>
<span>Hello 👋🏻</span>
<span>
{" "}
Je suis un développeur fullstack passioné par les automatisations,
la performance et la simplicité.
</span>
</p>
</section>
<section className="mt-8">
<p>
Je réalise actuellement un master informatique à{" "}
<UrlBadge url="https://www.mewo.fr" title="Mewo" />, effectué en
alternance chez{" "}
<UrlBadge url="https://www.sesamm.com/" title="SESAMm" /> où j'occupe
le poste de développeur fullstack depuis plus de 2ans.
</p>
</section>

<section className="mt-8">
<p>
En parallèle, je réalise des projets personnels que je documente sur
ce site. Vous pouvez retrouver mes{" "}
<span className="font-semibold">{postCount} articles</span> sur la
page <A href="/tag/all">blog</A>.
</p>
<p className="mt-2">
Pour l'instant, j'essaie d'apprendre{" "}
<UrlBadge url="https://www.rust-lang.org/" title="Rust" /> et{" "}
<UrlBadge url="https://go.dev/" title="Go" />.
</p>
</section>

<section className="mt-8">
<p>
En dehors de la programation, j'aime regarder des séries, lire des
livres et écouter de la musique.
</p>

<div className="grid grid-cols-1 sm:grid-cols-2 sm:mt-5 gap-2">
<div className="hidden sm:flex justify-end">
<CurrentlyListeningSvg />
</div>
<div className="sm:hidden font-semibold">
<p>En ce moment j'écoute:</p>
</div>
<SpotifyTopTrackBadge />
</div>
</section>

<section className="mt-8">
<p>
Vous pouvez me retrouver sur {/* TODO Download SVG and use it here */}
<UrlBadge url={GITHUB_PROFILE} title="GitHub" />{" "}
<UrlBadge url={TWITTER_PROFILE} title="Twitter" />
</p>
</section>
</main>
);
}
30 changes: 30 additions & 0 deletions src/components/CurrentlyListeningSvg.tsx

Large diffs are not rendered by default.

87 changes: 87 additions & 0 deletions src/components/SpotifyTopArtist.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { getSpotifyAccessToken } from "@/lib/spotify";
import Image from "next/image";
import { z } from "zod";

const API_URL =
"https://api.spotify.com/v1/me/top/tracks?time_range=short_term&limit=1&offset=0";

const itemSchema = z.object({
name: z.string(),
external_urls: z.object({
spotify: z.string(),
}),
artists: z.array(
z.object({
name: z.string(),
}),
),

album: z.object({
name: z.string(),
images: z.array(
z.object({
url: z.string(),
height: z.number(),
width: z.number(),
}),
),
}),
});
const itemsSchema = z.array(itemSchema);

async function getTopTrack() {
const token = await getSpotifyAccessToken();
const res = await fetch(API_URL, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!res.ok) {
console.error(await res.text());
throw new Error("Failed to fetch data from Spotify API");
}
const data = await res.json();
const items = itemsSchema.parse(data.items);
return items;
}

export async function SpotifyTopTrackBadge() {
const tracks = await getTopTrack();

if (tracks.length === 0) {
return null;
}

const topTrack = tracks[0];

return (
<a
className="flex flew-row rounded-md border border-neutral-200 dark:border-neutral-700 gap-4 hover:border-neutral-300 hover:dark:border-neutral-600 bg-accent text-accent-foreground p-2"
title="Ma musique préférée du moment"
href={topTrack.external_urls.spotify}
target="_blank"
rel="noopener noreferrer"
>
<Image
src={topTrack.album.images[0].url}
alt={topTrack.album.name}
width={64}
height={64}
unoptimized={true}
className="inline-block rounded-lg"
/>
<div>
<div className="font-semibold">{topTrack.name}</div>
<div className="text-sm text-neutral-700 dark:text-neutral-300">
{topTrack.artists.map((artist, i) => (
<span key={artist.name}>
{artist.name}
{i < topTrack.artists.length - 1 ? ", " : ""}
</span>
))}
</div>
</div>
</a>
);
}
32 changes: 32 additions & 0 deletions src/components/UrlBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { extractMetaTags } from "@/lib/scraper";
import Image from "next/image";

interface UrlBadgeProps {
url: string;
title: string;
favicon?: string;
}

export const UrlBadge = async ({ url, title, favicon }: UrlBadgeProps) => {
const metadata = favicon ? { favicon, title } : await extractMetaTags(url);

return (
<a
href={url}
className="rounded-md border border-neutral-200 dark:border-neutral-700 hover:border-neutral-300 hover:dark:border-neutral-600 bg-accent text-accent-foreground p-1 !no-underline space-x-2 leading-4 text-sm"
aria-label={title}
target="_blank"
rel="noopener noreferrer"
>
<Image
src={metadata.favicon}
alt={metadata.title}
width={16}
height={16}
unoptimized={true}
className="inline-block rounded-lg"
/>
<span>{title}</span>
</a>
);
};
8 changes: 8 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ export const env = createEnv({
server: {
NODE_ENV: z.enum(["development", "production"]),
BUILD_TIME: z.coerce.string(),

SPOTIFY_CLIENT_ID: z.string(),
SPOTIFY_CLIENT_SECRET: z.string(),
SPOTIFY_REFRESH_TOKEN: z.string(),
},

client: {
Expand All @@ -17,6 +21,10 @@ export const env = createEnv({
BUILD_TIME: process.env.BUILD_TIME,
NEXT_PUBLIC_BUILD_TIME: process.env.BUILD_TIME,
NEXT_PUBLIC_VERCEL_URL: process.env.NEXT_PUBLIC_VERCEL_URL,

SPOTIFY_CLIENT_ID: process.env.SPOTIFY_CLIENT_ID,
SPOTIFY_CLIENT_SECRET: process.env.SPOTIFY_CLIENT_SECRET,
SPOTIFY_REFRESH_TOKEN: process.env.SPOTIFY_REFRESH_TOKEN,
},

skipValidation: !!process.env.SKIP_ENV_VALIDATION,
Expand Down
5 changes: 1 addition & 4 deletions src/lib/mdx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,7 @@ const mdxComponents: MDXComponents = {
</h5>
),
p: ({ className, ...props }) => (
<p
className={cn("leading-7 [&:not(:first-child)]:mt-4", className)}
{...props}
/>
<p className={cn("[&:not(:first-child)]:mt-4", className)} {...props} />
),
ul: ({ className, ...props }) => (
<ul className={cn("mt-4 pl-4 md:pl-8 list-disc", className)} {...props} />
Expand Down
4 changes: 2 additions & 2 deletions src/lib/scraper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const domParser = new DOMParser({
errorHandler: {
warning: () => {},
error: () => {},
fatalError: console.error,
fatalError: console.warn,
},
});

Expand Down Expand Up @@ -76,7 +76,7 @@ export const extractMetaTags = async (url: string): Promise<MetaTags> => {
};

const EMPTY_BASE64_IMAGE =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkAAIAAAoAAv/lxKUAAAAASUVORK5CYII=";
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==";

const imageToBase64 = async (url: string) => {
if (!url) return EMPTY_BASE64_IMAGE;
Expand Down
46 changes: 46 additions & 0 deletions src/lib/spotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { env } from "@/env";
import cache from "memory-cache";

const getCachedAccessToken = () => {
const cachedToken = cache.get("spotifyAccessToken");
if (cachedToken) {
return cachedToken;
}
return null;
};

export async function getSpotifyAccessToken() {
const cachedToken = getCachedAccessToken();
if (cachedToken) {
return cachedToken;
}

const response = await fetch("https://accounts.spotify.com/api/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
client_id: env.SPOTIFY_CLIENT_ID,
client_secret: env.SPOTIFY_CLIENT_SECRET,
refresh_token: env.SPOTIFY_REFRESH_TOKEN,
}),
});

if (!response.ok) {
console.error(
"Failed to refresh Spotify access token",
await response.text(),
);
throw new Error("Failed to refresh Spotify access token");
}

const data = await response.json();
const { access_token, expires_in } = data;
cache.put("spotifyAccessToken", access_token, expires_in * 1000);

console.log("Spotify access token refreshed");

return access_token;
}
1 change: 1 addition & 0 deletions src/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
@layer components {
.post-content {
@apply md:max-w-3xl flex-1;
line-height: 1.75;
}
.larger-post-content {
@apply lg:-mx-24 xl:-mx-32;
Expand Down

0 comments on commit 22a3b97

Please sign in to comment.