Skip to content

Commit bd941b3

Browse files
committed
Update localization and refactor how app records are built
1 parent fce0650 commit bd941b3

File tree

16 files changed

+325
-239
lines changed

16 files changed

+325
-239
lines changed

astro.config.mjs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,16 @@
22
import { defineConfig } from 'astro/config';
33

44
// https://astro.build/config
5-
export default defineConfig({});
5+
export default defineConfig({
6+
// https://docs.astro.build/en/guides/internationalization/
7+
i18n: {
8+
locales: ["es", "en", "fr", "pt-br"],
9+
defaultLocale: "en",
10+
routing: {
11+
prefixDefaultLocale: true,
12+
},
13+
}
14+
15+
// https://docs.astro.build/en/guides/prefetch/
16+
//prefetch: true
17+
});

src/layouts/AppIndex.astro

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
import Layout from './Layout.astro';
3+
import { apps } from "@/lib/apps";
4+
5+
const appValues: AppInfo[] = Object.values(apps).sort(
6+
(a, b) => (b.rank ?? 0) - (a.rank ?? 0)
7+
);
8+
---
9+
<Layout title="Apps List">
10+
<main class="apps-index">
11+
<div class="hero">
12+
<h1>The App Fair Apps</h1>
13+
<p>A list of all the apps currently available through the App Fair Project</p>
14+
</div>
15+
16+
<div class="apps-container">
17+
<div class="apps-grid">
18+
{appValues.map((app) => (
19+
// TODO: we could pre-fetch with the attribute: data-astro-prefetch="hover"
20+
// see: https://docs.astro.build/en/guides/prefetch/
21+
// we would want to assess how much JS that adds to the page and any other overhead
22+
<a href={`app/${app.token}`} class="app-card">
23+
<div class="app-icon">
24+
<img src={app.ios?.appInfo?.iconURL} alt={`${app.ios?.appInfo?.name} icon`} />
25+
</div>
26+
<div class="app-info">
27+
<h2>{app.ios?.appInfo?.name}</h2>
28+
<p class="app-subtitle">{app.ios?.appInfo?.subtitle}</p>
29+
<div class="app-meta">
30+
<span class="category">{app.ios?.appInfo?.category}</span>
31+
<span class="rating">
32+
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
33+
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
34+
</svg>
35+
<!-- {app.rating} -->
36+
</span>
37+
</div>
38+
</div>
39+
</a>
40+
))}
41+
</div>
42+
</div>
43+
</main>
44+
</Layout>

src/layouts/AppInfo.astro

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
import Layout from './Layout.astro';
3+
import { apps, type AppInfo } from "@/lib/apps";
4+
5+
export async function getStaticPaths() {
6+
// the keys of the Record<string, AppInfo> are the app tokens
7+
return Object.keys(apps).map(tok => ({ params: { token: tok } }));
8+
}
9+
10+
const { token } = Astro.params;
11+
if (!token) {
12+
throw new Error(`No token parameter`);
13+
}
14+
15+
const tok: string = token;
16+
const app: AppInfo = apps[tok];
17+
//console.log(`app: ${app}`);
18+
19+
if (!app) {
20+
throw new Error(`App with token "${token}" not found`);
21+
}
22+
23+
const tokenLower = token.toLowerCase();
24+
25+
//console.log(`app.ios: ${app.ios}`);
26+
27+
const iosInfo = app.ios?.appInfo;
28+
console.log(`iosInfo: ${iosInfo}`);
29+
30+
//const fdroidInfo = app.fdroid?.appInfo; // TODO
31+
32+
const appStoreLink = app.ios?.appleItemId ? `https://apps.apple.com/app/${tokenLower}/id${app.ios?.appleItemId}` : null;
33+
const playStoreLink = app.android?.appid ? `https://play.google.com/store/apps/details?id=${app.android?.appid}` : null;
34+
35+
const appName = iosInfo?.name ?? app.token;
36+
const appSubtitle = iosInfo?.subtitle ?? "";
37+
const appDescription = iosInfo?.localizedDescription ?? "";
38+
const appIcon = iosInfo?.iconURL ?? "";
39+
40+
const sourceCodeURL = `https://github.com/${token}/${token}`;
41+
const privacyPolicyURL = "https://appfair.org/privacy";
42+
---
43+
<Layout title={appName}>
44+
<main class="app-detail">
45+
<div class="app-container">
46+
<!-- Header -->
47+
<div class="app-header">
48+
<div class="app-header-content">
49+
<div class="app-icon-large">
50+
<img src={appIcon} alt={`${appName} icon`} />
51+
</div>
52+
<div class="app-header-info force-wrap">
53+
<h1>{appName}</h1>
54+
<p class="subtitle">{appSubtitle}</p>
55+
<!-- <div class="developer">{app.developer}</div> -->
56+
</div>
57+
</div>
58+
59+
<!-- <div class="app-stats-bar">
60+
<div class="stat">
61+
<div class="stat-value">{app.rating}</div>
62+
<div class="stat-label">{app.reviews} Ratings</div>
63+
</div>
64+
<div class="stat-divider"></div>
65+
<div class="stat">
66+
<div class="stat-value">{app.age}</div>
67+
<div class="stat-label">Age</div>
68+
</div>
69+
<div class="stat-divider"></div>
70+
<div class="stat">
71+
<div class="stat-value">{app.size}</div>
72+
<div class="stat-label">Size</div>
73+
</div>
74+
</div> -->
75+
76+
<div class="download-section">
77+
{appStoreLink && (
78+
<a href={appStoreLink} class="btn-download btn-ios"><img src="https://appfair.org/assets/badges/apple-app-store.svg" alt="Download on the Apple App Store"/></a>
79+
)}
80+
{playStoreLink && (
81+
<a href={playStoreLink} class="btn-download btn-android"><img src="https://appfair.org/assets/badges/google-play-store.svg" alt="Download on the Google Play Store"/></a>
82+
)}
83+
</div>
84+
</div>
85+
86+
<!-- Screenshots -->
87+
<section class="screenshots-section">
88+
<h2>Screenshots</h2>
89+
<div class="screenshots-scroll">
90+
{iosInfo?.screenshots?.iphone?.map((screenshot, index) => (
91+
<div class="screenshot-item">
92+
<img src={screenshot} alt={`Screenshot ${index + 1}`} />
93+
</div>
94+
))}
95+
</div>
96+
</section>
97+
98+
<!-- Description -->
99+
<section class="description-section">
100+
<h2>About This App</h2>
101+
<pre>{appDescription}</pre>
102+
</section>
103+
104+
<!-- Permissions -->
105+
<section class="permissions-section force-wrap">
106+
<h2>Permissions</h2>
107+
<div class="permissions-grid">
108+
{iosInfo?.appPermissions?.privacy?.map((permission) => (
109+
<div class="permission-card">
110+
<div class="permission-icon">
111+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
112+
<circle cx="12" cy="12" r="10"></circle>
113+
<path d="M12 6v6l4 2"></path>
114+
</svg>
115+
</div>
116+
<div class="permission-content">
117+
<h3>{permission.name}</h3>
118+
<p>{permission.usageDescription}</p>
119+
</div>
120+
</div>
121+
)) || <p>None</p>}
122+
</div>
123+
<h2>Entitlements</h2>
124+
<div class="permissions-grid">
125+
{iosInfo?.appPermissions?.entitlements?.map((entitlement) => (
126+
<div class="permission-card">
127+
<div class="permission-icon">
128+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
129+
<circle cx="12" cy="12" r="10"></circle>
130+
<path d="M12 6v6l4 2"></path>
131+
</svg>
132+
</div>
133+
<div class="permission-content">
134+
<h3>{entitlement.name}</h3>
135+
</div>
136+
</div>
137+
)) || <p>None</p>}
138+
</div>
139+
</section>
140+
141+
<!-- Links -->
142+
<section class="links-section force-wrap">
143+
<h2>Developer Resources</h2>
144+
<div class="links-grid">
145+
<a href={sourceCodeURL} class="link-card" target="_blank" rel="noopener">
146+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
147+
<polyline points="16 18 22 12 16 6"></polyline>
148+
<polyline points="8 6 2 12 8 18"></polyline>
149+
</svg>
150+
<div>
151+
<h3>Source Code</h3>
152+
<p>View on GitHub</p>
153+
</div>
154+
</a>
155+
<a href={privacyPolicyURL} class="link-card" target="_blank" rel="noopener">
156+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
157+
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
158+
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
159+
</svg>
160+
<div>
161+
<h3>Privacy Policy</h3>
162+
<p>Learn about data protection</p>
163+
</div>
164+
</a>
165+
</div>
166+
</section>
167+
</div>
168+
</main>
169+
</Layout>

src/layouts/Layout.astro

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
---
2-
import '../styles/global.css';
2+
import '@/styles/global.css';
3+
4+
// ClientRouter provides nice transitions, but it breaks the light/dark mode selector
5+
// import { ClientRouter } from "astro:transitions";
36
47
interface Props {
58
title: string;
@@ -15,6 +18,23 @@ const { title } = Astro.props;
1518
<link rel="icon" type="image/png" href="/favicon.ico" />
1619
<meta name="generator" content={Astro.generator} />
1720
<title>{title} | The App Fair Project</title>
21+
{ /* <ClientRouter /> */ }
22+
<script>
23+
// Theme toggle functionality
24+
const themeToggle = document.getElementById('theme-toggle');
25+
const html = document.documentElement;
26+
// Load saved theme or default to the current prefers-color-scheme
27+
const defaultTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
28+
const savedTheme = localStorage.getItem('theme') || defaultTheme;
29+
html.setAttribute('data-theme', savedTheme);
30+
themeToggle?.addEventListener('click', () => {
31+
const currentTheme = html.getAttribute('data-theme');
32+
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
33+
html.setAttribute('data-theme', newTheme);
34+
localStorage.setItem('theme', newTheme);
35+
});
36+
</script>
37+
1838
</head>
1939
<body>
2040
<header class="site-header">
@@ -74,23 +94,5 @@ const { title } = Astro.props;
7494
</div>
7595
</div>
7696
</footer>
77-
78-
<script>
79-
// Theme toggle functionality
80-
const themeToggle = document.getElementById('theme-toggle');
81-
const html = document.documentElement;
82-
83-
// Load saved theme or default to light
84-
const savedTheme = localStorage.getItem('theme') || 'light';
85-
html.setAttribute('data-theme', savedTheme);
86-
87-
themeToggle?.addEventListener('click', () => {
88-
const currentTheme = html.getAttribute('data-theme');
89-
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
90-
91-
html.setAttribute('data-theme', newTheme);
92-
localStorage.setItem('theme', newTheme);
93-
});
94-
</script>
9597
</body>
9698
</html>

src/lib/apps.ts

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface AppInfo {
66
token: string;
77
ios?: DarwinAppInfo;
88
android?: AndroidAppInfo;
9+
rank?: int;
910
}
1011

1112
export interface DarwinAppInfo {
@@ -60,31 +61,35 @@ export interface AltAppVersion {
6061
size: int;
6162
}
6263

63-
var altstoreApps: AltAppInfo[] = altstoreData.apps;
64+
const altstoreApps: AltAppInfo[] = altstoreData.apps;
6465

65-
// merge the altstore app info into the app itself
66-
var allApps: AppInfo[] = appsData.apps;
66+
// index the AltAppInfo array by the bundleIdentifier
67+
const altstoreAppRecords: Record<string, AltAppInfo> = altstoreApps.reduce((acc, appInfo) => {
68+
acc[appInfo.bundleIdentifier] = appInfo;
69+
return acc;
70+
}, {} as Record<string, AltAppInfo>);
6771

68-
allApps.forEach((app: AppInfo) => {
69-
var iosApp = app.ios;
70-
if (iosApp === null) {
71-
return;
72-
}
73-
74-
console.log(`searching for ${iosApp.bundleId} in ${altstoreApps.length} altstore apps…`);
75-
var altStoreApp: AltAppInfo = altstoreApps.find(a => a.bundleIdentifier == iosApp.bundleId);
76-
console.log(`found: ${altStoreApp}`);
77-
78-
//app.ios.appInfo = null;
79-
if (altStoreApp) {
80-
iosApp.appInfo = altStoreApp;
81-
console.log(`assigned: ${altStoreApp}`);
82-
//app.ios.appInfo = altStoreApp;
83-
app.ios = iosApp;
84-
console.log(`matched: ${app.ios?.appInfo}`);
85-
}
72+
// merge the AltStore app info into the app itself
73+
const allApps: AppInfo[] = appsData.apps;
74+
75+
allApps.forEach((appInfo, index) => {
76+
// assign the rank as the index if it is not already set in the list
77+
appInfo.rank = appInfo.rank ?? index;
8678
});
8779

88-
// TODO: merge the F-Droid app info into the app
80+
const allAppRecords: Record<string, AppInfo> = allApps.reduce((acc, appInfo) => {
81+
// update the appInfo with the AltAppInfo
82+
let iosInfo = appInfo.ios;
83+
if (iosInfo != null) {
84+
iosInfo.appInfo = altstoreAppRecords[iosInfo.bundleId];
85+
//console.log(`iosInfo.appInfo for ${iosInfo.bundleId}: ${iosInfo.appInfo}`);
86+
appInfo.ios = iosInfo;
87+
}
88+
//console.log(`appInfo.ios.appInfo: ${appInfo.ios.appInfo}`);
89+
90+
// TODO: do the same with the fdroid index
91+
acc[appInfo.token] = appInfo;
92+
return acc;
93+
}, {} as Record<string, AppInfo>);
8994

90-
export const apps: AppInfo[] = allApps;
95+
export const apps: Record<string, AppInfo> = allAppRecords;

0 commit comments

Comments
 (0)