Initial commit: fristy.net personal site
Build & Push / Lint & Build (push) Successful in 1m15s
Build & Push / Build & Push Image (push) Successful in 52s

This commit is contained in:
fristy committed 2026-09-10 04:59:12 +03:00
commit ad943fff8c
51 files changed
+6052

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
const GITEA_API = "https://git.fristy.net/api/v1";
const GITEA_USER = "fristy";
const EXCLUDED_REPOS = new Set([".profile"]);
export interface GiteaRepo {
id: number;
name: string;
fullName: string;
description: string | null;
language: string | null;
htmlUrl: string;
website: string | null;
stars: number;
forks: number;
updatedAt: string;
}
interface GiteaApiRepo {
id: number;
name: string;
full_name: string;
description: string | null;
language: string | null;
html_url: string;
website: string;
stars_count: number;
forks_count: number;
updated_at: string;
}
export async function getRepos(): Promise<GiteaRepo[]> {
try {
const response = await fetch(
`${GITEA_API}/users/${GITEA_USER}/repos?limit=50&sort=updated&order=desc`,
{ next: { revalidate: 3600 } },
);
if (!response.ok) {
console.error(`cannot fetch gitea repos: ${response.status}`);
return [];
}
const data = (await response.json()) as GiteaApiRepo[];
return data
.filter((repo) => !EXCLUDED_REPOS.has(repo.name))
.map((repo) => ({
id: repo.id,
name: repo.name,
fullName: repo.full_name,
description: repo.description,
language: repo.language,
htmlUrl: repo.html_url,
website: repo.website || null,
stars: repo.stars_count,
forks: repo.forks_count,
updatedAt: repo.updated_at,
}));
} catch (error) {
console.error("cannot fetch gitea repos:", error);
return [];
}
}
+66
View File
@@ -0,0 +1,66 @@
{
"meta": {
"title": "fristy.net — fristy love you",
"description": "Fristys persönliche Ecke: Projekte, Live-Discord-Status und Kontakt."
},
"nav": {
"projects": "Projekte",
"discord": "Discord",
"contact": "Kontakt"
},
"hero": {
"greeting": "Hey, ich bin",
"tagline": "Ich programmiere als Hobby. Das ist meine persönliche Ecke: Projekte, Live-Discord-Status und Kontaktinfos.",
"ctaProjects": "Projekte Ansehen",
"ctaContact": "Kontakt"
},
"discord": {
"title": "Discord",
"live": "Live",
"online": "Online",
"idle": "Abwesend",
"dnd": "Bitte Nicht Stören",
"offline": "Offline",
"playing": "Spielt",
"listening": "Hört",
"customStatus": "Status",
"noActivity": "Gerade still... Hier läuft wohl nichts.",
"profile": "Profil Ansehen"
},
"projects": {
"title": "Projekte",
"subtitle": "Meine Open-Source-Projekte auf git.fristy.net. Die Liste aktualisiert sich automatisch.",
"updated": "Aktualisiert",
"stars": "Sterne",
"forks": "Forks",
"viewOnGitea": "Auf Gitea Öffnen",
"viewWebsite": "Webseite Öffnen",
"empty": "Noch keine öffentlichen Repos.",
"noDescription": "Keine Beschreibung."
},
"contact": {
"title": "Kontakt & Social",
"email": "E-Mail",
"discord": "Discord",
"github": "GitHub",
"gitea": "Gitea",
"spotify": "Spotify",
"steam": "Steam",
"copy": "Kopieren",
"copied": "Kopiert!"
},
"footer": {
"rights": "Alle Rechte vorbehalten",
"prefix": "Diese Seite ist",
"licenseLabel": "Apache 2.0",
"middle": "unter der",
"sourceLabel": "Open Source",
"suffix": "-Lizenz",
"licenseFirst": false
},
"notFound": {
"title": "Seite Nicht Gefunden",
"subtitle": "Hier gibt es keine Seite, aber du bist nicht verloren.",
"home": "Zurück Nach Hause"
}
}
+66
View File
@@ -0,0 +1,66 @@
{
"meta": {
"title": "fristy.net — fristy love you",
"description": "Fristy's personal corner: projects, live Discord status and contact."
},
"nav": {
"projects": "Projects",
"discord": "Discord",
"contact": "Contact"
},
"hero": {
"greeting": "Hey, I'm",
"tagline": "I write code as a hobby. This is my personal corner: my projects, live Discord status and contact info.",
"ctaProjects": "See Projects",
"ctaContact": "Contact"
},
"discord": {
"title": "Discord",
"live": "Live",
"online": "Online",
"idle": "Idle",
"dnd": "Do Not Disturb",
"offline": "Offline",
"playing": "Playing",
"listening": "Listening To",
"customStatus": "Status",
"noActivity": "Quiet right now... Nothing seems to be going on.",
"profile": "View Profile"
},
"projects": {
"title": "Projects",
"subtitle": "My open-source projects on git.fristy.net. The list updates automatically.",
"updated": "Updated",
"stars": "Stars",
"forks": "Forks",
"viewOnGitea": "Open on Gitea",
"viewWebsite": "Open Website",
"empty": "No public repos to show yet.",
"noDescription": "No description."
},
"contact": {
"title": "Contact & Social",
"email": "Email",
"discord": "Discord",
"github": "GitHub",
"gitea": "Gitea",
"spotify": "Spotify",
"steam": "Steam",
"copy": "Copy",
"copied": "Copied!"
},
"footer": {
"rights": "All rights reserved",
"prefix": "This site is",
"licenseLabel": "Apache 2.0",
"middle": "under the",
"sourceLabel": "open source",
"suffix": " license",
"licenseFirst": false
},
"notFound": {
"title": "Page Not Found",
"subtitle": "There's no page here, but you're not lost. Home is right there.",
"home": "Back Home"
}
}
+66
View File
@@ -0,0 +1,66 @@
{
"meta": {
"title": "fristy.net - fristy love you",
"description": "Fristy'nin kişisel köşesi: projeler, canlı Discord durumu ve iletişim."
},
"nav": {
"projects": "Projeler",
"discord": "Discord",
"contact": "İletişim"
},
"hero": {
"greeting": "Selam, ben",
"tagline": "Hobi olarak kod yazıyorum. Burası kişisel köşem: projelerim, anlık Discord durumum ve iletişim bilgilerim.",
"ctaProjects": "Projelere Bak",
"ctaContact": "İletişim"
},
"discord": {
"title": "Discord",
"live": "Canlı",
"online": "Çevrimiçi",
"idle": "Boşta",
"dnd": "Rahatsız Etmeyin",
"offline": "Çevrimdışı",
"playing": "Oynuyor",
"listening": "Dinliyor",
"customStatus": "Durum",
"noActivity": "Şu an sessizlik... Ortalıkta bir şey yok gibi.",
"profile": "Profile Git"
},
"projects": {
"title": "Projeler",
"subtitle": "Açık kaynak projelerim git.fristy.net üzerinde. Liste otomatik güncelleniyor.",
"updated": "Güncellendi",
"stars": "Yıldız",
"forks": "Çatal",
"viewOnGitea": "Gitea'da Aç",
"viewWebsite": "Web Sitesini Aç",
"empty": "Henüz gösterilecek public repo yok.",
"noDescription": "Açıklama yok."
},
"contact": {
"title": "İletişim ve Sosyal",
"email": "E-Posta",
"discord": "Discord",
"github": "GitHub",
"gitea": "Gitea",
"spotify": "Spotify",
"steam": "Steam",
"copy": "Kopyala",
"copied": "Kopyalandı!"
},
"footer": {
"rights": "Tüm Hakları Saklıdır",
"prefix": "Bu site",
"licenseLabel": "Apache 2.0",
"middle": "lisansı altında",
"sourceLabel": "açık kaynak",
"suffix": " kodludur",
"licenseFirst": true
},
"notFound": {
"title": "Sayfa Bulunamadı",
"subtitle": "Böyle bir sayfa yok, ama kaybolmadın. Ana sayfa hemen şurada.",
"home": "Ana Sayfaya Dön"
}
}
+12
View File
@@ -0,0 +1,12 @@
import type { Locale } from "./locales";
import tr from "./dictionaries/tr.json";
import en from "./dictionaries/en.json";
import de from "./dictionaries/de.json";
const dictionaries = { tr, en, de } as const;
export type Dictionary = typeof tr;
export function getDictionary(locale: Locale): Dictionary {
return dictionaries[locale] ?? dictionaries.tr;
}
+11
View File
@@ -0,0 +1,11 @@
import { cookies } from "next/headers";
import { DEFAULT_LOCALE, LOCALE_COOKIE, isLocale, type Locale } from "./locales";
export async function getLocale(): Promise<Locale> {
const store = await cookies();
const raw = store.get(LOCALE_COOKIE)?.value;
if (raw && isLocale(raw)) {
return raw;
}
return DEFAULT_LOCALE;
}
+48
View File
@@ -0,0 +1,48 @@
export const LOCALES = ["tr", "en", "de"] as const;
export type Locale = (typeof LOCALES)[number];
export const DEFAULT_LOCALE: Locale = "tr";
export const LOCALE_COOKIE = "locale";
export const LOCALE_LABELS: Record<Locale, string> = {
tr: "TR",
en: "EN",
de: "DE",
};
export function isLocale(value: string): value is Locale {
return (LOCALES as readonly string[]).includes(value);
}
export function matchLocale(acceptLanguage: string | null): Locale {
if (!acceptLanguage) {
return DEFAULT_LOCALE;
}
const preferences = acceptLanguage
.split(",")
.map((part) => {
const [range, ...params] = part.trim().split(";");
let q = 1;
for (const param of params) {
const match = param.trim().match(/^q=([0-9.]+)$/);
if (match) {
q = Number(match[1]);
}
}
return { range: range.toLowerCase(), q };
})
.sort((a, b) => b.q - a.q);
for (const { range } of preferences) {
if (range === "*") {
return DEFAULT_LOCALE;
}
const base = range.split("-")[0];
if (isLocale(base)) {
return base;
}
}
return DEFAULT_LOCALE;
}
+16
View File
@@ -0,0 +1,16 @@
"use server";
import { cookies } from "next/headers";
import { LOCALE_COOKIE, isLocale, type Locale } from "./locales";
export async function setLocaleCookie(locale: Locale): Promise<void> {
if (!isLocale(locale)) {
return;
}
const store = await cookies();
store.set(LOCALE_COOKIE, locale, {
path: "/",
maxAge: 60 * 60 * 24 * 365,
sameSite: "lax",
});
}
+108
View File
@@ -0,0 +1,108 @@
export const DISCORD_ID = "1014549805608992901";
const LANYARD_API = `https://api.lanyard.rest/v1/users/${DISCORD_ID}`;
export type DiscordStatus = "online" | "idle" | "dnd" | "offline";
export interface LanyardEmoji {
name: string;
id?: string;
animated?: boolean;
}
export interface LanyardActivityAssets {
large_image?: string;
large_text?: string;
small_image?: string;
small_text?: string;
}
export interface LanyardActivity {
id: string;
name: string;
type: number;
state?: string | null;
details?: string | null;
emoji?: LanyardEmoji | null;
timestamps?: { start?: number; end?: number } | null;
application_id?: string | null;
assets?: LanyardActivityAssets | null;
}
export function activityImageUrl(activity: LanyardActivity, small: boolean): string | null {
const key = small ? activity.assets?.small_image : activity.assets?.large_image;
if (!key) {
return null;
}
if (key.startsWith("mp:")) {
return `https://media.discordapp.net/external/${key.slice(3)}`;
}
if (key.startsWith("spotify:")) {
return `https://i.scdn.co/image/${key.slice("spotify:".length)}`;
}
if (activity.application_id) {
return `https://cdn.discordapp.com/app-assets/${activity.application_id}/${key}.png`;
}
return null;
}
export interface LanyardSpotify {
track_id: string;
song: string;
artist: string;
album: string;
album_art_url: string | null;
}
export interface LanyardData {
discord_user: {
id: string;
username: string;
global_name: string | null;
display_name: string | null;
discriminator: string;
avatar: string | null;
bot: boolean;
};
activities: LanyardActivity[];
discord_status: DiscordStatus;
active_on_discord_desktop: boolean;
active_on_discord_mobile: boolean;
active_on_discord_web: boolean;
listening_to_spotify: boolean;
spotify: LanyardSpotify | null;
}
interface LanyardResponse {
success: boolean;
data: LanyardData;
}
export async function getLanyard(): Promise<LanyardData | null> {
try {
const response = await fetch(LANYARD_API, { next: { revalidate: 10 } });
if (!response.ok) {
return null;
}
const json = (await response.json()) as LanyardResponse;
return json.success ? json.data : null;
} catch {
return null;
}
}
export function lanyardUrl(): string {
return LANYARD_API;
}
export function discordAvatarUrl(userId: string, avatarHash: string | null, size = 128): string {
if (!avatarHash) {
return `https://cdn.discordapp.com/embed/avatars/0.png?size=${size}`;
}
const extension = avatarHash.startsWith("a_") ? "gif" : "png";
return `https://cdn.discordapp.com/avatars/${userId}/${avatarHash}.${extension}?size=${size}`;
}
export function discordProfileUrl(userId: string): string {
return `https://discord.com/users/${userId}`;
}
+9
View File
@@ -0,0 +1,9 @@
export const SITE_URL = "https://fristy.net";
export const CONTACT_EMAIL = "iletisim@fristy.net";
export const GITEA_PROFILE_URL = "https://git.fristy.net/fristy";
export const SOURCE_URL = "https://git.fristy.net/fristy/fristynet";
export const GITHUB_URL = "https://github.com/fristyxd";
export const DISCORD_SERVER_URL = "https://discord.gg/fristy";
export const SPOTIFY_URL = "https://open.spotify.com/user/gayiw5j9i0rtvyra6gfzhpfwf";
export const STEAM_URL = "https://steamcommunity.com/id/fristylikeaillusion";
export const APACHE_LICENSE_URL = "https://www.apache.org/licenses/LICENSE-2.0";