Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .vitepress/config.mts → .vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig } from "vitepress";
import sidebar from "./sidebar.mjs";
import sidebar from "./sidebar";

// https://vitepress.dev/reference/site-config
export default defineConfig({
Expand Down
3 changes: 0 additions & 3 deletions .vitepress/sidebar.mts → .vitepress/sidebar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// import {DefaultTheme} from "vitepress";
// import Sidebar = DefaultTheme.Sidebar;

export default {
"/docs/": [
{
Expand Down
278 changes: 172 additions & 106 deletions .vitepress/theme/components/Contributors.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@
import { VPTeamMembers } from "vitepress/theme";
import { ref, computed } from "vue";

const props = defineProps({
lang: {
default: "en",
type: String
type LangString = "en" | "tr" | "de" | "pt" | "ru" | "zh";

const props = withDefaults(
defineProps<{
lang?: LangString;
}>(),
{
lang: "en"
}
});
interface LangString {
en: string;
tr: string;
de: string;
pt: string;
ru: string;
zh: string;
}
);

interface Contributor {
avatar: string;
Expand All @@ -24,64 +20,145 @@ interface Contributor {
links: Array<{ icon: string; link: string }>;
}

const members = ref<Contributor[]>([]);
const websiteMembers = ref<(Contributor | null)[]>([]);
const loaded = ref(false);
const websiteLoaded = ref(false);

const titleCreator = (<LangString>{
en: "Creator of Leaf",
tr: "Leaf'in Yaratıcısı",
de: "Schöpfer von Leaf",
pt: "Criador do Leaf",
ru: "Создатель Leaf",
zh: "Leaf 的创建者"
})[props.lang];
const titleCoreTeam = (<LangString>{
en: "Core team",
tr: "Ana ekip",
de: "Kernteam",
pt: "Equipe Principal",
ru: "Основная команда",
zh: "核心团队"
})[props.lang];
const titleWebDev = (<LangString>{
en: "Designer & Web Dev",
tr: "Tasarımcı & Web Geliştirici",
de: "Designer & Webentwickler",
pt: "Designer & Desenvolvedor Web",
ru: "Дизайнер и Веб-разработчик",
zh: "设计师兼网页开发"
})[props.lang];
const titleSpecial = (<LangString>{
en: "Special Supporter",
tr: "Özel Destekçi",
de: "Besonderer Unterstützer",
pt: "Apoiante Especial",
ru: "Особый поддерживающий",
zh: "特别支持者"
})[props.lang];

const rewrites = {
"Dreeam-qwq": { title: titleCreator },
HaHaWTH: { name: "Creeam (HaHaWTH)", title: titleCoreTeam },
//"Taiyou06": { title: titleCoreTeam },
hayanesuru: { title: titleCoreTeam },
MartijnMuijsers: { title: titleCoreTeam },
Pascalpex: { title: titleSpecial },
interface GitHubContributor {
login: string;
avatar_url: string;
html_url: string;
type: string;
}

type LocalizedText = Record<LangString, string>;
type Rewrite = Partial<Omit<Contributor, "links">> & {
links?: Contributor["links"];
};
type ContributorCache = {
timestamp: number;
data: GitHubContributor[];
};

const CACHE_TTL_MS = 30 * 60 * 1000;
const CACHE_KEY_PREFIX = "leaf-website:contributors:";

const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;

const isGitHubContributor = (value: unknown): value is GitHubContributor => {
if (!isRecord(value)) return false;

return (
typeof value.login === "string" &&
typeof value.avatar_url === "string" &&
typeof value.html_url === "string" &&
typeof value.type === "string"
);
};

const getUserContributors = (data: unknown): GitHubContributor[] => {
if (!Array.isArray(data)) return [];
return data.filter(isGitHubContributor).filter((m) => m.type === "User");
};

const isContributorCache = (value: unknown): value is ContributorCache => {
if (!isRecord(value)) return false;

return typeof value.timestamp === "number" && Array.isArray(value.data);
};

const getCacheKey = (repo: string) => `${CACHE_KEY_PREFIX}${repo}`;

const readCachedContributors = (repo: string): GitHubContributor[] | null => {
if (typeof window === "undefined") return null;

try {
const raw = window.localStorage.getItem(getCacheKey(repo));
if (!raw) return null;

const parsed: unknown = JSON.parse(raw);
if (!isContributorCache(parsed)) return null;
if (Date.now() - parsed.timestamp > CACHE_TTL_MS) {
window.localStorage.removeItem(getCacheKey(repo));
return null;
}

return getUserContributors(parsed.data);
} catch {
return null;
}
};

const writeCachedContributors = (repo: string, data: GitHubContributor[]) => {
if (typeof window === "undefined") return;

try {
const payload: ContributorCache = {
timestamp: Date.now(),
data
};
window.localStorage.setItem(getCacheKey(repo), JSON.stringify(payload));
} catch {}
};

const localizedTitles = {
creator: {
en: "Creator of Leaf",
tr: "Leaf'in Yaratıcısı",
de: "Schöpfer von Leaf",
pt: "Criador do Leaf",
ru: "Создатель Leaf",
zh: "Leaf 的创建者"
},
coreTeam: {
en: "Core team",
tr: "Ana ekip",
de: "Kernteam",
pt: "Equipe Principal",
ru: "Основная команда",
zh: "核心团队"
},
webDev: {
en: "Designer & Web Dev",
tr: "Tasarımcı & Web Geliştirici",
de: "Designer & Webentwickler",
pt: "Designer & Desenvolvedor Web",
ru: "Дизайнер и Веб-разработчик",
zh: "设计师兼网页开发"
},
special: {
en: "Special Supporter",
tr: "Özel Destekçi",
de: "Besonderer Unterstützer",
pt: "Apoiante Especial",
ru: "Особый поддерживающий",
zh: "特别支持者"
}
} satisfies Record<string, LocalizedText>;

type TitleKey = keyof typeof localizedTitles;

const title = (key: TitleKey) => localizedTitles[key][props.lang];

const rewrites = computed<Record<string, Rewrite>>(() => ({
"Dreeam-qwq": { title: title("creator") },
HaHaWTH: { name: "Creeam (HaHaWTH)", title: title("coreTeam") },
//"Taiyou06": { title: title("coreTeam") },
hayanesuru: { title: title("coreTeam") },
MartijnMuijsers: { title: title("coreTeam") },
Pascalpex: { title: title("special") },
envizar: {
title: titleWebDev,
title: title("webDev"),
links: [{ icon: "telegram", link: "https://t.me/envizar" }]
}
};
}));

const repoMembers = ref<GitHubContributor[]>([]);
const websiteRepoMembers = ref<GitHubContributor[]>([]);

const transform = ({ login, avatar_url, html_url }: any) => {
const transform = ({ login, avatar_url, html_url }: GitHubContributor, rewrite?: Rewrite): Contributor => {
const base = {
avatar: avatar_url,
name: login,
links: [{ icon: "github", link: html_url }]
};
const rewrite = rewrites[login];

return rewrite
? {
...base,
Expand All @@ -91,56 +168,45 @@ const transform = ({ login, avatar_url, html_url }: any) => {
: base;
};

const transformWebsite = ({ login, avatar_url, html_url }: any) => {
if (login in rewrites) {
return null; // Skip this contributor if already in main list with custom title
}
const allMembers = computed<Contributor[]>(() => {
const rewriteMap = rewrites.value;
const mainMembers = repoMembers.value.map((m) => transform(m, rewriteMap[m.login]));
const mainNames = new Set(mainMembers.map((m) => m.name));

const base = {
avatar: avatar_url,
name: login,
links: [{ icon: "github", link: html_url }]
};
return base;
};
const websiteMembers = websiteRepoMembers.value
.filter((m) => !(m.login in rewriteMap))
.map((m) => transform(m))
.filter((m) => !mainNames.has(m.name));

// Combine both contributor lists, filtering out nulls and duplicates
const allMembers = computed(() => {
if (!loaded.value || !websiteLoaded.value) return members.value;
return [...mainMembers, ...websiteMembers];
});

const mainContributors = new Set(members.value.map((m) => m.name));
const filteredWebsiteMembers = websiteMembers.value.filter((m) => m !== null && !mainContributors.has(m.name));
const fetchRepoContributors = async (repo: string): Promise<GitHubContributor[]> => {
const cached = readCachedContributors(repo);
if (cached) return cached;

return [...members.value, ...filteredWebsiteMembers];
});
try {
const resp = await fetch(`https://api.github.com/repos/Winds-Studio/${repo}/contributors`);
const data: unknown = await resp.json();

// Fetch main repo contributors
fetch("https://api.github.com/repos/Winds-Studio/Leaf/contributors")
.then((resp) => resp.json())
.then((data) => {
// TODO: find a solution to avoid rate limit
if (Array.isArray(data)) {
members.value = data.filter((m) => m.type == "User").map(transform);
} else {
console.warn(`Unexpected response: ${JSON.stringify(data)}`);
members.value = [];
if (!Array.isArray(data)) {
console.warn(`Unexpected response from ${repo}: ${JSON.stringify(data)}`);
return [];
}
})
.finally(() => (loaded.value = true));

// Fetch website repo contributors
fetch("https://api.github.com/repos/Winds-Studio/Leaf-website/contributors")
.then((resp) => resp.json())
.then((data) => {
// TODO: find a solution to avoid rate limit
if (Array.isArray(data)) {
websiteMembers.value = data.filter((m) => m.type == "User").map(transform);
} else {
console.warn(`Unexpected response: ${JSON.stringify(data)}`);
websiteMembers.value = [];
}
})
.finally(() => (websiteLoaded.value = true));

const contributors = getUserContributors(data);
writeCachedContributors(repo, contributors);
return contributors;
} catch (error) {
console.warn(`Failed to fetch contributors from ${repo}:`, error);
return [];
}
};

void Promise.all([fetchRepoContributors("Leaf"), fetchRepoContributors("Leaf-website")]).then(([main, website]) => {
repoMembers.value = main;
websiteRepoMembers.value = website;
});
</script>

<template>
Expand Down
Loading
Loading