Skip to content

Commit 83cdac2

Browse files
authored
Merge pull request #59 from String-sg/feat/about
Feat/about
2 parents 14d9200 + 903c578 commit 83cdac2

6 files changed

Lines changed: 185 additions & 2 deletions

File tree

api/apps.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { apps, featuredApps, bumpRules, appSubmissions } from '../src/db/schema'
44
import { eq, desc, and, gte, lte } from 'drizzle-orm';
55

66
export const config = {
7-
runtime: 'edge',
7+
runtime: 'nodejs',
88
};
99

1010
const OPAL_CANONICAL_LOGO = '/icons/opal2.png';
@@ -29,7 +29,7 @@ export default async function handler(_request: Request) {
2929
.select()
3030
.from(apps)
3131
.orderBy(desc(apps.frequency));
32-
const allApps = allAppsRaw.map(normalizeOpalLogo);
32+
const officialApps = allAppsRaw.map(normalizeOpalLogo);
3333

3434
// Get all approved submissions
3535
const approvedSubmissions = await db

public/highlights-14str.jpg

470 KB
Loading

public/meetupatcrater2022.png

661 KB
Loading

public/meetupatgoogle2023.png

721 KB
Loading

src/components/About.tsx

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { useState } from 'react';
2+
import { AuthButton } from './AuthButton';
3+
import { navigateTo } from './Router';
4+
5+
function useTheme() {
6+
const [isDark, setIsDark] = useState(() => {
7+
try {
8+
return localStorage.getItem('string-theme') === 'dark';
9+
} catch {
10+
return false;
11+
}
12+
});
13+
14+
const toggle = () => setIsDark((d) => !d);
15+
const t = (light: string, dark: string) => (isDark ? dark : light);
16+
17+
return { isDark, toggle, t };
18+
}
19+
20+
function Header({ isDark, onToggleTheme }: { isDark: boolean; onToggleTheme: () => void }) {
21+
return (
22+
<header className="bg-string-dark sticky top-0 z-20">
23+
<div className="max-w-7xl mx-auto px-4 sm:px-6 py-3 flex items-center justify-between">
24+
<button onClick={() => navigateTo('/')} className="cursor-pointer">
25+
<img src="/logo-green.svg" alt="String" className="h-7" />
26+
</button>
27+
<div className="flex items-center gap-2 sm:gap-3">
28+
<button
29+
onClick={onToggleTheme}
30+
className="p-2 rounded-lg transition-colors hover:bg-string-darker text-gray-400"
31+
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
32+
>
33+
{isDark ? (
34+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
35+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
36+
</svg>
37+
) : (
38+
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
39+
<path strokeLinecap="round" strokeLinejoin="round" d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
40+
</svg>
41+
)}
42+
</button>
43+
<AuthButton />
44+
</div>
45+
</div>
46+
</header>
47+
);
48+
}
49+
50+
export function About() {
51+
const { isDark, toggle: toggleTheme, t } = useTheme();
52+
53+
return (
54+
<div className={`min-h-screen ${t('bg-string-bg', 'bg-string-darker')}`}>
55+
<Header isDark={isDark} onToggleTheme={toggleTheme} />
56+
57+
<main className="max-w-4xl mx-auto px-4 sm:px-6 py-8 space-y-6">
58+
{/* Intro */}
59+
<div className={`${t('bg-white', 'bg-[#2a2d30]')} rounded-2xl p-8 shadow-sm text-center`}>
60+
<h1 className={`text-3xl font-bold mb-4 ${t('text-string-dark', 'text-white')}`}>About</h1>
61+
<p className={`${t('text-string-text-primary', 'text-gray-300')}`}>
62+
String surfaces good products and workflows used by public officers.
63+
</p>
64+
<p className={`${t('text-string-text-primary', 'text-gray-300')}`}>
65+
Share best practices and help to increase operational efficiency with tech together.
66+
</p>
67+
</div>
68+
69+
{/* Mission */}
70+
<div className={`${t('bg-white', 'bg-[#2a2d30]')} rounded-2xl p-8 shadow-sm text-center`}>
71+
<p className={`text-sm mb-2 ${t('text-string-text-secondary', 'text-gray-400')}`}>Aspirationally, we are a:</p>
72+
<h2 className="text-2xl font-bold text-string-mint mb-3">Tech ecosystem builder</h2>
73+
<p className={`max-w-2xl mx-auto ${t('text-string-text-primary', 'text-gray-300')}`}>
74+
We run tech explainers, build products and establish partnerships to drive adoption of more efficient solutions for education officers (previously for all public officers).
75+
</p>
76+
</div>
77+
78+
{/* Meetup photos */}
79+
<div className={`${t('bg-white', 'bg-[#2a2d30]')} rounded-2xl p-8 shadow-sm space-y-8`}>
80+
<div>
81+
<img src="/highlights-14str.jpg" alt="String's meetup at Open Government Products" className="w-full rounded-xl shadow-sm mb-2" />
82+
<p className={`text-sm text-center ${t('text-string-text-secondary', 'text-gray-400')}`}>
83+
String's meetup at Open Government Products, 18 Nov 2024 |{' '}
84+
<a href="https://www.linkedin.com/feed/update/urn:li:activity:7265594854934487040/" target="_blank" rel="noopener noreferrer" className="text-string-mint hover:underline">
85+
Read more
86+
</a>
87+
</p>
88+
</div>
89+
<div className="flex flex-col md:flex-row gap-6">
90+
<div className="md:w-1/2">
91+
<img src="/meetupatcrater2022.png" alt="Meetup at Crater @ NYGH" className="w-full rounded-xl shadow-sm mb-2" />
92+
<p className={`text-sm text-center ${t('text-string-text-secondary', 'text-gray-400')}`}>
93+
Meetup at Crater @ NYGH (Dec 2022) |{' '}
94+
<a href="https://www.linkedin.com/feed/update/urn:li:activity:7008581328241655808/" target="_blank" rel="noopener noreferrer" className="text-string-mint hover:underline">
95+
Read more
96+
</a>
97+
</p>
98+
</div>
99+
<div className="md:w-1/2">
100+
<img src="/meetupatgoogle2023.png" alt="Meetup @ Google APAC HQ" className="w-full rounded-xl shadow-sm mb-2" />
101+
<p className={`text-sm text-center ${t('text-string-text-secondary', 'text-gray-400')}`}>
102+
Meetup @ Google APAC HQ (Jun 2023) |{' '}
103+
<a href="https://www.linkedin.com/feed/update/urn:li:activity:7072765894686343168/" target="_blank" rel="noopener noreferrer" className="text-string-mint hover:underline">
104+
Read more
105+
</a>
106+
</p>
107+
</div>
108+
</div>
109+
</div>
110+
111+
{/* Year in Review */}
112+
<div className={`${t('bg-white', 'bg-[#2a2d30]')} rounded-2xl p-8 shadow-sm`}>
113+
<h2 className={`text-2xl font-bold mb-6 ${t('text-string-dark', 'text-white')}`}>Year in Review</h2>
114+
<div className="flex flex-col sm:flex-row gap-4">
115+
<a
116+
href="https://medium.com/string/it-happened-on-string-e7f0a5df81f3"
117+
target="_blank"
118+
rel="noopener noreferrer"
119+
className={`flex-1 rounded-xl p-5 border transition-colors ${t('border-string-border hover:bg-string-surface-hover', 'border-gray-600 hover:bg-[#33373b]')}`}
120+
>
121+
<p className="text-string-mint font-semibold mb-1">2024 Recap</p>
122+
<p className={`text-sm ${t('text-string-text-secondary', 'text-gray-400')}`}>It happened on String — a look back at 2024 →</p>
123+
</a>
124+
<a
125+
href="https://medium.com/it-happened-on-string-2025-09836fdef4b3"
126+
target="_blank"
127+
rel="noopener noreferrer"
128+
className={`flex-1 rounded-xl p-5 border transition-colors ${t('border-string-border hover:bg-string-surface-hover', 'border-gray-600 hover:bg-[#33373b]')}`}
129+
>
130+
<p className="text-string-mint font-semibold mb-1">2025 Recap</p>
131+
<p className={`text-sm ${t('text-string-text-secondary', 'text-gray-400')}`}>It happened on String — a look back at 2025 →</p>
132+
</a>
133+
</div>
134+
</div>
135+
136+
{/* CTAs */}
137+
<div className={`${t('bg-white', 'bg-[#2a2d30]')} rounded-2xl p-8 shadow-sm`}>
138+
<h2 className={`text-2xl font-bold mb-6 text-center ${t('text-string-dark', 'text-white')}`}>Get in Touch</h2>
139+
<div className="flex flex-col sm:flex-row gap-4 justify-center">
140+
<a
141+
href="http://join.string.sg/"
142+
target="_blank"
143+
rel="noopener noreferrer"
144+
className="inline-flex items-center justify-center gap-2 bg-string-mint text-string-dark font-semibold py-3 px-6 rounded-xl hover:bg-string-mint-light transition-colors"
145+
>
146+
Join String
147+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
148+
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
149+
</svg>
150+
</a>
151+
<a
152+
href="http://reports.string.sg/"
153+
target="_blank"
154+
rel="noopener noreferrer"
155+
className={`inline-flex items-center justify-center gap-2 font-semibold py-3 px-6 rounded-xl border transition-colors ${t('border-string-border text-string-dark hover:bg-string-surface-hover', 'border-gray-600 text-gray-300 hover:bg-[#33373b]')}`}
156+
>
157+
Product metrics
158+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
159+
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
160+
</svg>
161+
</a>
162+
<a
163+
href="https://discord.gg/ZRHqBtwh9b"
164+
target="_blank"
165+
rel="noopener noreferrer"
166+
className={`inline-flex items-center justify-center gap-2 font-semibold py-3 px-6 rounded-xl border transition-colors ${t('border-string-border text-string-dark hover:bg-string-surface-hover', 'border-gray-600 text-gray-300 hover:bg-[#33373b]')}`}
167+
>
168+
Discord
169+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
170+
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
171+
</svg>
172+
</a>
173+
</div>
174+
</div>
175+
</main>
176+
</div>
177+
);
178+
}

src/components/Router.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
22
import { UserDashboard } from './UserDashboard';
33
import { Terms } from './Terms';
44
import { Privacy } from './Privacy';
5+
import { About } from './About';
56
import { PersonalProfile } from './PersonalProfile';
67
import { DevProfileMock } from './DevProfileMock';
78
import { isReservedSlug } from '../lib/slug-utils';
@@ -35,6 +36,10 @@ export function Router({ children }: RouterProps) {
3536
return <Privacy />;
3637
}
3738

39+
if (currentPath === '/about') {
40+
return <About />;
41+
}
42+
3843
// Check for user profile routes (e.g., /lee-kh)
3944
// Must be a single path segment (no nested routes)
4045
if (currentPath !== '/' && currentPath.startsWith('/')) {

0 commit comments

Comments
 (0)