22import { VPTeamMembers } from " vitepress/theme" ;
33import { ref , computed } from " vue" ;
44
5- const props = defineProps ({
6- lang: {
7- default: " en" ,
8- type: String
5+ type LangString = " en" | " tr" | " de" | " pt" | " ru" | " zh" ;
6+
7+ const props = withDefaults (
8+ defineProps <{
9+ lang? : LangString ;
10+ }>(),
11+ {
12+ lang: " en"
913 }
10- });
11- interface LangString {
12- en: string ;
13- tr: string ;
14- de: string ;
15- pt: string ;
16- ru: string ;
17- zh: string ;
18- }
14+ );
1915
2016interface Contributor {
2117 avatar: string ;
@@ -24,64 +20,145 @@ interface Contributor {
2420 links: Array <{ icon: string ; link: string }>;
2521}
2622
27- const members = ref <Contributor []>([]);
28- const websiteMembers = ref <(Contributor | null )[]>([]);
29- const loaded = ref (false );
30- const websiteLoaded = ref (false );
31-
32- const titleCreator = (<LangString >{
33- en: " Creator of Leaf" ,
34- tr: " Leaf'in Yaratıcısı" ,
35- de: " Schöpfer von Leaf" ,
36- pt: " Criador do Leaf" ,
37- ru: " Создатель Leaf" ,
38- zh: " Leaf 的创建者"
39- })[props .lang ];
40- const titleCoreTeam = (<LangString >{
41- en: " Core team" ,
42- tr: " Ana ekip" ,
43- de: " Kernteam" ,
44- pt: " Equipe Principal" ,
45- ru: " Основная команда" ,
46- zh: " 核心团队"
47- })[props .lang ];
48- const titleWebDev = (<LangString >{
49- en: " Designer & Web Dev" ,
50- tr: " Tasarımcı & Web Geliştirici" ,
51- de: " Designer & Webentwickler" ,
52- pt: " Designer & Desenvolvedor Web" ,
53- ru: " Дизайнер и Веб-разработчик" ,
54- zh: " 设计师兼网页开发"
55- })[props .lang ];
56- const titleSpecial = (<LangString >{
57- en: " Special Supporter" ,
58- tr: " Özel Destekçi" ,
59- de: " Besonderer Unterstützer" ,
60- pt: " Apoiante Especial" ,
61- ru: " Особый поддерживающий" ,
62- zh: " 特别支持者"
63- })[props .lang ];
64-
65- const rewrites = {
66- " Dreeam-qwq" : { title: titleCreator },
67- HaHaWTH: { name: " Creeam (HaHaWTH)" , title: titleCoreTeam },
68- // "Taiyou06": { title: titleCoreTeam },
69- hayanesuru: { title: titleCoreTeam },
70- MartijnMuijsers: { title: titleCoreTeam },
71- Pascalpex: { title: titleSpecial },
23+ interface GitHubContributor {
24+ login: string ;
25+ avatar_url: string ;
26+ html_url: string ;
27+ type: string ;
28+ }
29+
30+ type LocalizedText = Record <LangString , string >;
31+ type Rewrite = Partial <Omit <Contributor , " links" >> & {
32+ links? : Contributor [" links" ];
33+ };
34+ type ContributorCache = {
35+ timestamp: number ;
36+ data: GitHubContributor [];
37+ };
38+
39+ const CACHE_TTL_MS = 30 * 60 * 1000 ;
40+ const CACHE_KEY_PREFIX = " leaf-website:contributors:" ;
41+
42+ const isRecord = (value : unknown ): value is Record <string , unknown > => typeof value === " object" && value !== null ;
43+
44+ const isGitHubContributor = (value : unknown ): value is GitHubContributor => {
45+ if (! isRecord (value )) return false ;
46+
47+ return (
48+ typeof value .login === " string" &&
49+ typeof value .avatar_url === " string" &&
50+ typeof value .html_url === " string" &&
51+ typeof value .type === " string"
52+ );
53+ };
54+
55+ const getUserContributors = (data : unknown ): GitHubContributor [] => {
56+ if (! Array .isArray (data )) return [];
57+ return data .filter (isGitHubContributor ).filter ((m ) => m .type === " User" );
58+ };
59+
60+ const isContributorCache = (value : unknown ): value is ContributorCache => {
61+ if (! isRecord (value )) return false ;
62+
63+ return typeof value .timestamp === " number" && Array .isArray (value .data );
64+ };
65+
66+ const getCacheKey = (repo : string ) => ` ${CACHE_KEY_PREFIX }${repo } ` ;
67+
68+ const readCachedContributors = (repo : string ): GitHubContributor [] | null => {
69+ if (typeof window === " undefined" ) return null ;
70+
71+ try {
72+ const raw = window .localStorage .getItem (getCacheKey (repo ));
73+ if (! raw ) return null ;
74+
75+ const parsed: unknown = JSON .parse (raw );
76+ if (! isContributorCache (parsed )) return null ;
77+ if (Date .now () - parsed .timestamp > CACHE_TTL_MS ) {
78+ window .localStorage .removeItem (getCacheKey (repo ));
79+ return null ;
80+ }
81+
82+ return getUserContributors (parsed .data );
83+ } catch {
84+ return null ;
85+ }
86+ };
87+
88+ const writeCachedContributors = (repo : string , data : GitHubContributor []) => {
89+ if (typeof window === " undefined" ) return ;
90+
91+ try {
92+ const payload: ContributorCache = {
93+ timestamp: Date .now (),
94+ data
95+ };
96+ window .localStorage .setItem (getCacheKey (repo ), JSON .stringify (payload ));
97+ } catch {}
98+ };
99+
100+ const localizedTitles = {
101+ creator: {
102+ en: " Creator of Leaf" ,
103+ tr: " Leaf'in Yaratıcısı" ,
104+ de: " Schöpfer von Leaf" ,
105+ pt: " Criador do Leaf" ,
106+ ru: " Создатель Leaf" ,
107+ zh: " Leaf 的创建者"
108+ },
109+ coreTeam: {
110+ en: " Core team" ,
111+ tr: " Ana ekip" ,
112+ de: " Kernteam" ,
113+ pt: " Equipe Principal" ,
114+ ru: " Основная команда" ,
115+ zh: " 核心团队"
116+ },
117+ webDev: {
118+ en: " Designer & Web Dev" ,
119+ tr: " Tasarımcı & Web Geliştirici" ,
120+ de: " Designer & Webentwickler" ,
121+ pt: " Designer & Desenvolvedor Web" ,
122+ ru: " Дизайнер и Веб-разработчик" ,
123+ zh: " 设计师兼网页开发"
124+ },
125+ special: {
126+ en: " Special Supporter" ,
127+ tr: " Özel Destekçi" ,
128+ de: " Besonderer Unterstützer" ,
129+ pt: " Apoiante Especial" ,
130+ ru: " Особый поддерживающий" ,
131+ zh: " 特别支持者"
132+ }
133+ } satisfies Record <string , LocalizedText >;
134+
135+ type TitleKey = keyof typeof localizedTitles ;
136+
137+ const title = (key : TitleKey ) => localizedTitles [key ][props .lang ];
138+
139+ const rewrites = computed <Record <string , Rewrite >>(() => ({
140+ " Dreeam-qwq" : { title: title (" creator" ) },
141+ HaHaWTH: { name: " Creeam (HaHaWTH)" , title: title (" coreTeam" ) },
142+ // "Taiyou06": { title: title("coreTeam") },
143+ hayanesuru: { title: title (" coreTeam" ) },
144+ MartijnMuijsers: { title: title (" coreTeam" ) },
145+ Pascalpex: { title: title (" special" ) },
72146 envizar: {
73- title: titleWebDev ,
147+ title: title ( " webDev " ) ,
74148 links: [{ icon: " telegram" , link: " https://t.me/envizar" }]
75149 }
76- };
150+ }));
151+
152+ const repoMembers = ref <GitHubContributor []>([]);
153+ const websiteRepoMembers = ref <GitHubContributor []>([]);
77154
78- const transform = ({ login , avatar_url , html_url }: any ) => {
155+ const transform = ({ login , avatar_url , html_url }: GitHubContributor , rewrite ? : Rewrite ) : Contributor => {
79156 const base = {
80157 avatar: avatar_url ,
81158 name: login ,
82159 links: [{ icon: " github" , link: html_url }]
83160 };
84- const rewrite = rewrites [ login ];
161+
85162 return rewrite
86163 ? {
87164 ... base ,
@@ -91,56 +168,45 @@ const transform = ({ login, avatar_url, html_url }: any) => {
91168 : base ;
92169};
93170
94- const transformWebsite = ({ login , avatar_url , html_url } : any ) => {
95- if ( login in rewrites ) {
96- return null ; // Skip this contributor if already in main list with custom title
97- }
171+ const allMembers = computed < Contributor []>(( ) => {
172+ const rewriteMap = rewrites . value ;
173+ const mainMembers = repoMembers . value . map (( m ) => transform ( m , rewriteMap [ m . login ]));
174+ const mainNames = new Set ( mainMembers . map (( m ) => m . name ));
98175
99- const base = {
100- avatar: avatar_url ,
101- name: login ,
102- links: [{ icon: " github" , link: html_url }]
103- };
104- return base ;
105- };
176+ const websiteMembers = websiteRepoMembers .value
177+ .filter ((m ) => ! (m .login in rewriteMap ))
178+ .map ((m ) => transform (m ))
179+ .filter ((m ) => ! mainNames .has (m .name ));
106180
107- // Combine both contributor lists, filtering out nulls and duplicates
108- const allMembers = computed (() => {
109- if (! loaded .value || ! websiteLoaded .value ) return members .value ;
181+ return [... mainMembers , ... websiteMembers ];
182+ });
110183
111- const mainContributors = new Set (members .value .map ((m ) => m .name ));
112- const filteredWebsiteMembers = websiteMembers .value .filter ((m ) => m !== null && ! mainContributors .has (m .name ));
184+ const fetchRepoContributors = async (repo : string ): Promise <GitHubContributor []> => {
185+ const cached = readCachedContributors (repo );
186+ if (cached ) return cached ;
113187
114- return [... members .value , ... filteredWebsiteMembers ];
115- });
188+ try {
189+ const resp = await fetch (` https://api.github.com/repos/Winds-Studio/${repo }/contributors ` );
190+ const data: unknown = await resp .json ();
116191
117- // Fetch main repo contributors
118- fetch (" https://api.github.com/repos/Winds-Studio/Leaf/contributors" )
119- .then ((resp ) => resp .json ())
120- .then ((data ) => {
121- // TODO: find a solution to avoid rate limit
122- if (Array .isArray (data )) {
123- members .value = data .filter ((m ) => m .type == " User" ).map (transform );
124- } else {
125- console .warn (` Unexpected response: ${JSON .stringify (data )} ` );
126- members .value = [];
192+ if (! Array .isArray (data )) {
193+ console .warn (` Unexpected response from ${repo }: ${JSON .stringify (data )} ` );
194+ return [];
127195 }
128- })
129- .finally (() => (loaded .value = true ));
130-
131- // Fetch website repo contributors
132- fetch (" https://api.github.com/repos/Winds-Studio/Leaf-website/contributors" )
133- .then ((resp ) => resp .json ())
134- .then ((data ) => {
135- // TODO: find a solution to avoid rate limit
136- if (Array .isArray (data )) {
137- websiteMembers .value = data .filter ((m ) => m .type == " User" ).map (transform );
138- } else {
139- console .warn (` Unexpected response: ${JSON .stringify (data )} ` );
140- websiteMembers .value = [];
141- }
142- })
143- .finally (() => (websiteLoaded .value = true ));
196+
197+ const contributors = getUserContributors (data );
198+ writeCachedContributors (repo , contributors );
199+ return contributors ;
200+ } catch (error ) {
201+ console .warn (` Failed to fetch contributors from ${repo }: ` , error );
202+ return [];
203+ }
204+ };
205+
206+ void Promise .all ([fetchRepoContributors (" Leaf" ), fetchRepoContributors (" Leaf-website" )]).then (([main , website ]) => {
207+ repoMembers .value = main ;
208+ websiteRepoMembers .value = website ;
209+ });
144210 </script >
145211
146212<template >
0 commit comments