street-lingo/apps/indonesian-app/src/App.vue

343 lines
7.0 KiB
Vue

<template>
<div id="app">
<header class="header">
<div class="header-content">
<div class="logo-section">
<div class="logo">
<span class="flag">🇮🇩</span>
<h1>Learn Indonesian</h1>
</div>
<p class="tagline">Learn Indonesian through everyday scenarios</p>
</div>
</div>
</header>
<nav class="scenario-nav">
<div class="nav-container">
<div class="nav-tabs">
<a
v-for="scenario in scenarios"
:key="scenario.type"
@click="handleScenarioSwitch(scenario.type)"
:class="['nav-tab', { active: $route.params.type === scenario.type }]"
href="#"
>
<span class="tab-emoji">{{ scenario.emoji }}</span>
<span class="tab-text">{{ scenario.name }}</span>
</a>
</div>
</div>
</nav>
<main class="main-content">
<router-view />
</main>
</div>
</template>
<script>
export default {
name: 'App',
data() {
return {
scenarios: [],
hasConversationProgress: false
}
},
provide() {
return {
updateConversationProgress: this.updateConversationProgress
}
},
async mounted() {
await this.loadScenarios()
},
methods: {
async loadScenarios() {
try {
const response = await fetch('/api/scenarios/indonesian')
const scenariosData = await response.json()
this.scenarios = Object.entries(scenariosData).map(([type, scenario]) => ({
type: type,
name: scenario.title,
emoji: this.getScenarioEmoji(type),
description: scenario.description,
challenge: scenario.challenge,
goal: scenario.goal
}))
} catch (error) {
console.error('Failed to load scenarios:', error)
this.scenarios = [
{ type: 'warung', name: 'At a Warung', emoji: '🍜' },
{ type: 'ojek', name: 'Taking an Ojek', emoji: '🏍️' },
{ type: 'alfamart', name: 'At Alfamart', emoji: '🏪' },
{ type: 'coffee_shop', name: 'Coffee Shop Small Talk', emoji: '☕' }
]
}
},
getScenarioEmoji(type) {
const emojiMap = {
'warung': '🍜',
'ojek': '🏍️',
'alfamart': '🏪',
'coffee_shop': '☕'
}
return emojiMap[type] || '📍'
},
updateConversationProgress(hasProgress) {
this.hasConversationProgress = hasProgress
},
handleScenarioSwitch(newScenarioType) {
event.preventDefault()
if (this.$route.params.type === newScenarioType) {
return
}
if (this.hasConversationProgress) {
const confirmed = confirm(
`Switching scenarios will reset your current conversation progress.\n\nAre you sure you want to switch to "${this.scenarios.find(s => s.type === newScenarioType)?.name}"?`
)
if (!confirmed) {
return
}
}
this.$router.push(`/scenario/${newScenarioType}`)
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Crimson+Pro:wght@400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #c53030;
--primary-light: #e53e3e;
--primary-dark: #9c1c1c;
--secondary: #dd6b20;
--accent: #38a169;
--surface: #fefcf7;
--surface-alt: #f7f5f0;
--surface-dark: #2d3748;
--text: #2d3748;
--text-light: #4a5568;
--text-muted: #718096;
--border: #e2d8cc;
--shadow-sm: 0 1px 2px 0 rgb(45 55 72 / 0.05);
--shadow: 0 4px 6px -1px rgb(45 55 72 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(45 55 72 / 0.1);
--radius: 12px;
--radius-lg: 16px;
}
body {
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
background: var(--surface);
min-height: 100vh;
color: var(--text);
font-feature-settings: 'liga' 1, 'kern' 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: var(--surface);
border-bottom: 1px solid var(--border);
padding: 1.5rem 0;
position: sticky;
top: 0;
z-index: 100;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.logo-section {
text-align: center;
}
.logo {
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.flag {
font-size: 2rem;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
}
.logo h1 {
font-family: 'Crimson Pro', serif;
font-size: 2rem;
font-weight: 600;
color: var(--text);
letter-spacing: -0.02em;
}
.tagline {
font-size: 1rem;
color: var(--text-light);
font-weight: 400;
letter-spacing: 0.01em;
}
.scenario-nav {
background: var(--surface-alt);
border-bottom: 1px solid var(--border);
padding: 1rem 0;
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
}
.nav-tabs {
display: flex;
gap: 0.5rem;
justify-content: center;
flex-wrap: wrap;
}
.nav-tab {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.25rem;
border-radius: var(--radius);
text-decoration: none;
color: var(--text-light);
font-weight: 500;
font-size: 0.9rem;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
background: var(--surface);
border: 1px solid var(--border);
position: relative;
overflow: hidden;
}
.nav-tab::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
opacity: 0;
transition: opacity 0.2s ease;
z-index: -1;
}
.nav-tab:hover {
color: var(--text);
border-color: var(--primary-light);
box-shadow: var(--shadow-sm);
transform: translateY(-1px);
}
.nav-tab.active {
color: white;
background: var(--primary);
border-color: var(--primary);
box-shadow: var(--shadow);
}
.nav-tab.active::before {
opacity: 0;
}
.tab-emoji {
font-size: 1.1rem;
filter: grayscale(0.3);
transition: filter 0.2s ease;
}
.nav-tab:hover .tab-emoji,
.nav-tab.active .tab-emoji {
filter: grayscale(0);
}
.tab-text {
white-space: nowrap;
}
.main-content {
flex: 1;
padding: 2rem;
max-width: 1400px;
margin: 0 auto;
width: 100%;
}
@media (max-width: 768px) {
.header-content,
.nav-container {
padding: 0 1rem;
}
.logo h1 {
font-size: 1.75rem;
}
.tagline {
font-size: 0.9rem;
}
.nav-tabs {
gap: 0.25rem;
}
.nav-tab {
padding: 0.625rem 1rem;
font-size: 0.85rem;
}
.main-content {
padding: 1rem;
}
}
@media (max-width: 480px) {
.logo {
flex-direction: column;
gap: 0.5rem;
}
.nav-tabs {
flex-direction: column;
align-items: center;
}
.nav-tab {
width: 100%;
max-width: 200px;
justify-content: center;
}
}
</style>