Skip to content

Commit 305c28e

Browse files
author
Daniel Sullivan
authored
Merge pull request #5 from daniel-sullivan/move-to-mui
Add PWA option
2 parents 02f9f7c + 6641fea commit 305c28e

6 files changed

Lines changed: 190 additions & 42 deletions

File tree

frontend/dist/index.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,14 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>BabelBridge</title>
77
<meta name="color-scheme" content="light dark" />
8-
<script type="module" crossorigin src="/assets/index-CFPVen-_.js"></script>
8+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
9+
<link rel="manifest" href="/manifest.json" />
10+
<meta name="theme-color" content="#6366F1" />
11+
<meta name="apple-mobile-web-app-capable" content="yes" />
12+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
13+
<meta name="apple-mobile-web-app-title" content="BabelBridge" />
14+
<link rel="apple-touch-icon" href="/favicon.svg" />
15+
<script type="module" crossorigin src="/assets/index-CcXe80mT.js"></script>
916
</head>
1017
<body>
1118
<div id="root"></div>

frontend/index.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<title>BabelBridge</title>
77
<meta name="color-scheme" content="light dark" />
8+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
9+
<link rel="manifest" href="/manifest.json" />
10+
<meta name="theme-color" content="#6366F1" />
11+
<meta name="apple-mobile-web-app-capable" content="yes" />
12+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
13+
<meta name="apple-mobile-web-app-title" content="BabelBridge" />
14+
<link rel="apple-touch-icon" href="/favicon.svg" />
815
</head>
916
<body>
1017
<div id="root"></div>

frontend/public/favicon.svg

Lines changed: 16 additions & 0 deletions
Loading

frontend/public/manifest.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "BabelBridge",
3+
"short_name": "BabelBridge",
4+
"description": "A modern web-based translation tool with live language identification and pluggable AI backends",
5+
"start_url": "/",
6+
"display": "standalone",
7+
"background_color": "#0f1220",
8+
"theme_color": "#6366F1",
9+
"orientation": "portrait-primary",
10+
"icons": [
11+
{
12+
"src": "/favicon.svg",
13+
"sizes": "any",
14+
"type": "image/svg+xml",
15+
"purpose": "any maskable"
16+
}
17+
],
18+
"categories": ["productivity", "utilities"],
19+
"shortcuts": [
20+
{
21+
"name": "New Translation",
22+
"short_name": "Translate",
23+
"description": "Start a new translation",
24+
"url": "/",
25+
"icons": [
26+
{
27+
"src": "/favicon.svg",
28+
"sizes": "any",
29+
"type": "image/svg+xml"
30+
}
31+
]
32+
}
33+
]
34+
}
35+

frontend/src/App.tsx

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { TranslationComposer } from './components/TranslationComposer'
66
import { TranslationOutput } from './components/TranslationOutput'
77
import { TranslationHistory } from './components/TranslationHistory'
88
import { ModalsContainer } from './components/ModalsContainer'
9-
import { toLanguageName } from './utils/languages'
9+
import { AddToAppButton } from './components/AddToAppButton'
1010
import logoUrl from './assets/logo.svg'
1111

1212
function AppContent() {
@@ -40,11 +40,12 @@ function AppContent() {
4040

4141
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 3 }}>
4242
<TranslationOutput />
43+
<AddToAppButton />
4344
<TranslationHistory />
4445
</Box>
4546
</Container>
4647

47-
<AppFooter targetLang={context.targetLang} />
48+
<AppFooter />
4849
<ModalsContainer />
4950
</Box>
5051
)
@@ -87,15 +88,7 @@ function AppHeader() {
8788
)
8889
}
8990

90-
interface AppFooterProps {
91-
targetLang: string
92-
}
93-
94-
function AppFooter({ targetLang }: AppFooterProps) {
95-
// Default to Japanese if no target language is set
96-
const displayLang = targetLang || 'ja'
97-
const flagEmoji = getFlagEmoji(displayLang)
98-
91+
function AppFooter() {
9992
return (
10093
<Box
10194
component="footer"
@@ -104,28 +97,16 @@ function AppFooter({ targetLang }: AppFooterProps) {
10497
px: 3,
10598
display: 'flex',
10699
alignItems: 'center',
107-
gap: 1,
100+
justifyContent: 'center',
108101
borderTop: '1px solid',
109102
borderColor: 'divider',
110103
backgroundColor: 'background.paper',
111104
}}
112105
>
113-
<Typography variant="body2" color="text.secondary">
114-
Last target: {toLanguageName(displayLang)} {flagEmoji}
115-
</Typography>
116-
<Box
117-
sx={{
118-
width: 4,
119-
height: 4,
120-
borderRadius: '50%',
121-
backgroundColor: 'text.secondary',
122-
opacity: 0.5,
123-
}}
124-
/>
125106
<Typography
126107
variant="body2"
127108
component="a"
128-
href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie"
109+
href="https://github.com/daniel-sullivan/babel-bridge"
129110
target="_blank"
130111
rel="noreferrer"
131112
sx={{
@@ -136,27 +117,12 @@ function AppFooter({ targetLang }: AppFooterProps) {
136117
},
137118
}}
138119
>
139-
Session via cookie
120+
© 2025 Daniel Sullivan
140121
</Typography>
141122
</Box>
142123
)
143124
}
144125

145-
function getFlagEmoji(langCode: string): string {
146-
const flags: Record<string, string> = {
147-
ja: '🇯🇵',
148-
es: '🇪🇸',
149-
de: '🇩🇪',
150-
en: '🇺🇸',
151-
fr: '🇫🇷',
152-
it: '🇮🇹',
153-
pt: '🇵🇹',
154-
zh: '🇨🇳',
155-
ko: '🇰🇷',
156-
ru: '🇷🇺'
157-
}
158-
return flags[langCode.split('-')[0]] || '🌐'
159-
}
160126

161127
export default function App() {
162128
return (
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import React, { useState, useEffect } from 'react'
2+
import { Box, Button, Snackbar, Alert, IconButton } from '@mui/material'
3+
import { GetApp, Close, PhoneIphone, LaptopMac } from '@mui/icons-material'
4+
5+
interface BeforeInstallPromptEvent extends Event {
6+
prompt(): Promise<void>
7+
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
8+
}
9+
10+
export function AddToAppButton() {
11+
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null)
12+
const [showIOSInstructions, setShowIOSInstructions] = useState(false)
13+
const [isInstallable, setIsInstallable] = useState(false)
14+
15+
useEffect(() => {
16+
const handleBeforeInstallPrompt = (e: Event) => {
17+
// Prevent the mini-infobar from appearing on mobile
18+
e.preventDefault()
19+
setDeferredPrompt(e as BeforeInstallPromptEvent)
20+
setIsInstallable(true)
21+
}
22+
23+
const handleAppInstalled = () => {
24+
setIsInstallable(false)
25+
setDeferredPrompt(null)
26+
}
27+
28+
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
29+
window.addEventListener('appinstalled', handleAppInstalled)
30+
31+
return () => {
32+
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt)
33+
window.removeEventListener('appinstalled', handleAppInstalled)
34+
}
35+
}, [])
36+
37+
const isIOS = () => {
38+
return /iPad|iPhone|iPod/.test(navigator.userAgent)
39+
}
40+
41+
const isInStandaloneMode = () => {
42+
return window.matchMedia('(display-mode: standalone)').matches ||
43+
(window.navigator as any).standalone === true
44+
}
45+
46+
const handleInstallClick = async () => {
47+
if (isIOS()) {
48+
setShowIOSInstructions(true)
49+
return
50+
}
51+
52+
if (deferredPrompt) {
53+
deferredPrompt.prompt()
54+
const { outcome } = await deferredPrompt.userChoice
55+
if (outcome === 'accepted') {
56+
setDeferredPrompt(null)
57+
setIsInstallable(false)
58+
}
59+
}
60+
}
61+
62+
// Don't show the button if already installed or not installable
63+
if (isInStandaloneMode() || (!isInstallable && !isIOS())) {
64+
return null
65+
}
66+
67+
return (
68+
<>
69+
<Box
70+
sx={{
71+
display: 'flex',
72+
justifyContent: 'center',
73+
mt: 2,
74+
mb: 1,
75+
}}
76+
>
77+
<Button
78+
variant="outlined"
79+
startIcon={isIOS() ? <PhoneIphone /> : <LaptopMac />}
80+
onClick={handleInstallClick}
81+
sx={{
82+
borderRadius: 2,
83+
textTransform: 'none',
84+
px: 3,
85+
py: 1,
86+
}}
87+
>
88+
Add to {isIOS() ? 'Home Screen' : 'Desktop'}
89+
</Button>
90+
</Box>
91+
92+
{/* iOS Installation Instructions */}
93+
<Snackbar
94+
open={showIOSInstructions}
95+
onClose={() => setShowIOSInstructions(false)}
96+
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
97+
autoHideDuration={8000}
98+
>
99+
<Alert
100+
severity="info"
101+
sx={{ maxWidth: '90vw' }}
102+
action={
103+
<IconButton
104+
size="small"
105+
color="inherit"
106+
onClick={() => setShowIOSInstructions(false)}
107+
>
108+
<Close fontSize="small" />
109+
</IconButton>
110+
}
111+
>
112+
To install: tap the Share button in Safari, then tap "Add to Home Screen"
113+
</Alert>
114+
</Snackbar>
115+
</>
116+
)
117+
}

0 commit comments

Comments
 (0)