Initial commit: fristy.net personal site
This commit is contained in:
51 files changed
+6052
No files matched your search
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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}`;
|
||||
}
|
||||
@@ -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";
|
||||
Reference in new issue
Block a user