added i18n, english and french for now

This commit is contained in:
2026-01-03 23:01:15 +01:00
parent f71d1ff9ac
commit 53b854d03a
19 changed files with 715 additions and 207 deletions

View File

@@ -53,3 +53,14 @@ export async function getLastRoom(): Promise<string | null> {
const s = await getStore()
return (await s.get<string>('last_room_uuid')) ?? null
}
export async function saveLocalePreference(locale: string) {
const s = await getStore()
await s.set('language', locale)
await s.save()
}
export async function getLocalePreference(): Promise<string | null> {
const s = await getStore()
return (await s.get<string>('language')) ?? null
}

View File

@@ -4,7 +4,7 @@
<div v-if="uuid === 'none'" class="no-room">
<div class="empty-state">
<i class="fa-solid fa-comments"></i>
<p>Select a room to start talking</p>
<p>{{ $t('chat-no-room') }}</p>
</div>
</div>
@@ -15,7 +15,7 @@
<div class="input-container">
<button v-if="isOwner && !currentRoom?.global" class="invite-btn" @click="showInviteModal = true"
title="Invite people">
:title="$t('chat-invite-title')">
<i class="fa-solid fa-users"></i>
</button>

View File

@@ -1,22 +1,22 @@
<template>
<div class="backdrop" @click.self="emit('close')">
<form class="modal" @submit.prevent="submit">
<h2>Create room</h2>
<h2>{{ $t('chat-create-title') }}</h2>
<input v-model="name" placeholder="Room name" autofocus />
<input v-model="name" :placeholder="$t('chat-create-name-placeholder')" autofocus />
<label class="checkbox">
<input type="checkbox" v-model="global" />
<span>Global room</span>
<span>{{ $t('chat-create-global') }}</span>
</label>
<div class="actions">
<button type="button" @click="emit('close')" class="secondary">
Cancel
{{ $t('shared-cancel') }}
</button>
<button type="submit">
Create
{{ $t('chat-create-submit') }}
</button>
</div>
</form>

View File

@@ -1,27 +1,27 @@
<template>
<div class="backdrop" @click.self="emit('close')">
<form class="modal" @submit.prevent="submit">
<h2>Invite People</h2>
<h2>{{ $t('chat-invite-title') }}</h2>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<div class="input-group">
<label>Receiver username</label>
<label>{{ $t('chat-invite-receiver') }}</label>
<input v-model="receiverUsername" placeholder="username" autofocus />
</div>
<label class="checkbox">
<input type="checkbox" v-model="requestFriend" />
<span>Also send a friend request</span>
<span>{{ $t('chat-invite-friend-too') }}</span>
</label>
<div class="actions">
<button type="button" @click="emit('close')" class="secondary">
Cancel
{{ $t('shared-cancel') }}
</button>
<button type="submit">
Send
{{ $t('chat-invite-send') }}
</button>
</div>
</form>
@@ -32,6 +32,9 @@
import { ref } from 'vue'
import { sendRoomInvite } from '../api/rooms'
import { sendFriendRequest } from '../api/friends';
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const props = defineProps<{ room_uuid: string }>();
@@ -64,8 +67,7 @@ async function submit() {
emit('close')
} catch (err: any) {
console.error(err)
errorMessage.value = err?.message || err || 'An error occurred'
errorMessage.value = err?.message || err || $t('shared-error');
}
}
</script>

View File

@@ -1,3 +1,8 @@
<template>
<textarea ref="textareaRef" v-model="content" @input="resize" @keydown="handleKeydown" rows="1"
:placeholder="$t('chat-input-placeholder')"></textarea>
</template>
<script setup lang="ts">
import { ref, nextTick } from 'vue'
@@ -46,11 +51,6 @@ function handleKeydown(e: KeyboardEvent) {
}
</script>
<template>
<textarea ref="textareaRef" v-model="content" @input="resize" @keydown="handleKeydown" placeholder="type a message"
rows="1"></textarea>
</template>
<style scoped>
textarea {
width: 100%;

View File

@@ -1,24 +1,24 @@
<template>
<div>
<nav id="bottom-nav">
<router-link to="/" class="nav-item" :class="{ 'router-link-active': $route.name === 'chat' }">
<i class="fa-solid fa-message"></i>
</router-link>
<div>
<nav id="bottom-nav">
<router-link to="/" class="nav-item" :class="{ 'router-link-active': $route.name === 'chat' }">
<i class="fa-solid fa-message"></i>
</router-link>
<router-link to="/friendlist" class="nav-item">
<i class="fa-solid fa-user-group"></i>
</router-link>
<router-link to="/friendlist" class="nav-item">
<i class="fa-solid fa-user-group"></i>
</router-link>
<router-link to="/notifications" class="nav-item">
<i class="fa-solid fa-bell"></i>
<span v-if="totalCount > 0" class="badge">{{ totalCount }}</span>
</router-link>
<router-link to="/notifications" class="nav-item">
<i class="fa-solid fa-bell"></i>
<span v-if="totalCount > 0" class="badge">{{ totalCount }}</span>
</router-link>
<router-link to="/account" class="nav-item">
<i class="fa-solid fa-circle-user"></i>
</router-link>
</nav>
</div>
<router-link to="/account" class="nav-item">
<i class="fa-solid fa-circle-user"></i>
</router-link>
</nav>
</div>
</template>
<script setup lang="ts">
@@ -28,85 +28,85 @@ import { useNotifications } from '../store'
const { totalCount, refreshNotifications } = useNotifications()
onMounted(() => {
refreshNotifications()
refreshNotifications()
})
</script>
<style scoped>
#bottom-nav {
display: flex;
gap: 28px;
padding: 5px 22px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 100vh;
z-index: 50;
user-select: none;
-webkit-user-select: none;
display: flex;
gap: 28px;
padding: 5px 22px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 100vh;
z-index: 50;
user-select: none;
-webkit-user-select: none;
}
.nav-item {
all: unset;
all: unset;
position: relative;
cursor: pointer;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
border-radius: 100vh;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
position: relative;
cursor: pointer;
width: 44px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
border-radius: 100vh;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
}
.nav-item i {
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
transition:
color 0.2s ease,
background-color 0.2s ease,
transform 0.15s ease;
}
.badge {
position: absolute;
top: 4px;
right: 4px;
background-color: var(--accent);
color: black;
font-size: 0.65rem;
font-weight: bold;
min-width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
border: 2px solid var(--panel);
pointer-events: none;
position: absolute;
top: 4px;
right: 4px;
background-color: var(--accent);
color: black;
font-size: 0.65rem;
font-weight: bold;
min-width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
border: 2px solid var(--panel);
pointer-events: none;
}
@media (hover: hover) {
.nav-item:hover {
background: rgba(255, 255, 255, 0.04);
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.04);
}
.nav-item:not(.router-link-active):hover i {
color: var(--text);
}
.nav-item:not(.router-link-active):hover i {
color: var(--text);
}
}
.router-link-active i {
color: var(--accent);
color: var(--accent);
}
@media (max-width: 720px) {
#bottom-nav {
bottom: 16px;
}
#bottom-nav {
bottom: 16px;
}
}
</style>

View File

@@ -1,9 +1,10 @@
<template>
<div class="room-list">
<header class="rooms-header">
<h2>Rooms</h2>
<button class="create-btn" @click="showCreate = true" title="Create a room"><i
class="fa-solid fa-plus"></i></button>
<h2>{{ $t('chat-room-list-title') }}</h2>
<button class="create-btn" @click="showCreate = true" :title="$t('chat-create-title')">
<i class="fa-solid fa-plus"></i>
</button>
</header>
<Teleport to="body">
@@ -15,7 +16,7 @@
:class="{ active: route.params.uuid === room.uuid }" @click="emit('select-room')">
<div class="room-info">
<span class="room-name">{{ room.name }}</span>
<span class="room-owner">by {{ room.owner_name }}</span>
<span class="room-owner">{{ $t('chat-room-owner', { owner: room.owner_name }) }}</span>
</div>
</router-link>
</div>

View File

@@ -1,34 +1,34 @@
<template>
<div class="backdrop" @click.self="emit('close')">
<form class="modal" @submit.prevent="submit">
<h2>Update your Account</h2>
<p class="subtitle">Only fill in the fields you wish to change.</p>
<h2>{{ $t('account-update-modal-title') }}</h2>
<p class="subtitle">{{ $t('account-update-subtitle') }}</p>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<div class="input-group">
<label>Username</label>
<input v-model="username" placeholder="New username" autofocus />
<label>{{ $t('auth-username') }}</label>
<input v-model="username" :placeholder="$t('auth-username')" autofocus />
</div>
<div class="input-group">
<label>Email</label>
<input v-model="email" type="email" placeholder="New email" />
<label>{{ $t('auth-email') }}</label>
<input v-model="email" type="email" :placeholder="$t('auth-email')" />
</div>
<div class="input-group">
<label>New Password</label>
<input v-model="password" type="password" placeholder="Leave blank to keep current" />
<input v-model="confirmPassword" type="password" placeholder="Confirm new password" />
<label>{{ $t('account-new-password') }}</label>
<input v-model="password" type="password" placeholder="..." />
<input v-model="confirmPassword" type="password" :placeholder="$t('account-new-password-confirm')" />
</div>
<div class="actions">
<button type="button" @click="emit('close')" class="secondary" :disabled="isSubmitting">
Cancel
{{ $t('shared-cancel') }}
</button>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? 'Updating...' : 'Save Changes' }}
{{ isSubmitting ? $t('account-updating') : $t('account-update-save') }}
</button>
</div>
</form>
@@ -40,6 +40,9 @@ import { ref } from 'vue'
import { updateAccount } from '../api/account'
import { updateLocalUser } from '../authStore'
import type { User } from '../types'
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const props = defineProps<{ user: User | null }>()
const emit = defineEmits(['close', 'updated'])
@@ -56,13 +59,13 @@ async function submit() {
errorMessage.value = ''
if (password.value !== confirmPassword.value) {
errorMessage.value = "Passwords do not match."
errorMessage.value = $t('auth-error-password-match')
return
}
// Prevent empty submission
if (!username.value || !email.value) {
errorMessage.value = "Username and Email are required."
errorMessage.value = $t('account-error-required')
return
}
@@ -81,7 +84,7 @@ async function submit() {
emit('updated')
emit('close')
} catch (err: any) {
errorMessage.value = err?.message || 'Update failed'
errorMessage.value = err?.message || $t('account-error-failed')
} finally {
isSubmitting.value = false
}

66
src/i18n.ts Normal file
View File

@@ -0,0 +1,66 @@
import { FluentBundle, FluentResource } from '@fluent/bundle';
import { createFluentVue } from 'fluent-vue';
import enMessages from './locales/en.ftl?raw';
import frMessages from './locales/fr.ftl?raw';
const MESSAGES: Record<string, string> = {
en: enMessages,
fr: frMessages,
// es: esMessages,
};
const BUNDLES: Record<string, FluentBundle> = {};
// Helper to create and cache bundles
function getBundle(locale: string): FluentBundle {
if (BUNDLES[locale]) return BUNDLES[locale];
const bundle = new FluentBundle(locale);
const resource = new FluentResource(MESSAGES[locale] || MESSAGES['en']);
bundle.addResource(resource);
BUNDLES[locale] = bundle;
return bundle;
}
const fallbackBundle = getBundle('en');
export const fluent = createFluentVue({
bundles: [fallbackBundle]
});
/**
* Matches input (e.g., 'fr-FR' or 'fr') against supported keys.
*/
export function setLanguage(localeCode: string) {
// Extract base language (e.g., 'en' from 'en-US')
const base = localeCode.split('-')[0].toLowerCase();
// Check if supported
const target = MESSAGES[localeCode] ? localeCode : (MESSAGES[base] ? base : 'en');
const mainBundle = getBundle(target);
fluent.bundles = [mainBundle, fallbackBundle];
return target;
}
export const supportedLocales = Object.keys(MESSAGES);
/**
* Iterates through supported locales and gets the value of the 'language' key
* from each specific bundle.
*/
export function getSupportedLanguagesMetadata() {
return supportedLocales.map(code => {
const bundle = getBundle(code);
const message = bundle.getMessage('language');
let nativeName = code.toUpperCase();
if (message && message.value) {
nativeName = bundle.formatPattern(message.value);
}
return { code, name: nativeName };
});
}

79
src/locales/en.ftl Normal file
View File

@@ -0,0 +1,79 @@
## Language
language = English
## Navigation
nav-chat = Chat
nav-friends = Friendlist
nav-notifications = Notifications
nav-account = Account
## Auth
auth-login-title = Login
auth-register-title = Register
auth-email = Email
auth-username = Username
auth-password = Password
auth-confirm-password = confirm password
auth-update-password =
auth-login-btn = Login
auth-register-btn = Create Account
auth-no-account = Don't have an account?
auth-has-account = Already have an account?
auth-error-unknown = An unknown error occurred
auth-error-password-match = Passwords do not match
auth-error-password-length = Password must be at least 8 characters long
auth-error-email-invalid = Please enter a valid email address
## Chat page
chat-no-room = Select a room to start talking
chat-input-placeholder = type a message
chat-invite-title = Invite People
chat-invite-receiver = Receiver username
chat-invite-friend-too = Also send a friend request
chat-invite-send = Send
chat-room-list-title = Rooms
chat-room-owner = by {$owner}
chat-create-title = Create room
chat-create-name-placeholder = Room name
chat-create-global = Global room
chat-create-submit = Create
## Friends page
friends-title = Your friends
friends-add-title = Add Friend
friends-send-request = Send Request
friends-list-header = Your Friends
friends-error-required = Username is required.
## Notifications page
notifications-title = Notifications
notifications-friend-requests = Friend Requests
notifications-room-invites = Room Invites
notifications-no-requests = No pending requests
notifications-no-invites = No pending invites
notifications-accept = Accept
notifications-join = Join
notifications-invite-from = from: {$user}
notifications-error-friend = An error occurred while accepting the request.
notifications-error-room = An error occurred while accepting the invite.
## Account page
account-title = Your Account
account-label-username = Username:
account-label-email = Email:
account-update-btn = Update
account-logout-btn = Logout
account-loading = Loading account details...
account-update-modal-title = Update your Account
account-update-subtitle = Only fill in the fields you wish to change.
account-new-password = New Password
account-new-password-confirm = Confirm new password
account-update-save = Save Changes
account-updating = Updating...
account-language = Language
account-error-required = Username and Email are required.
account-error-failed = Update failed
## Shared
shared-cancel = Cancel
shared-error = An error occurred

78
src/locales/fr.ftl Normal file
View File

@@ -0,0 +1,78 @@
## Language
language = Français
## Navigation
nav-chat = Messagerie
nav-friends = Amis
nav-notifications = Notifications
nav-account = Compte
## Auth
auth-login-title = Connexion
auth-register-title = Inscription
auth-email = Email
auth-username = Nom d'utilisateur
auth-password = Mot de passe
auth-confirm-password = confirmer le mot de passe
auth-login-btn = Se connecter
auth-register-btn = Créer un compte
auth-no-account = Pas encore de compte ?
auth-has-account = Déjà un compte ?
auth-error-unknown = Une erreur inconnue est survenue
auth-error-password-match = Les mots de passe ne correspondent pas
auth-error-password-length = Le mot de passe doit faire au moins 8 caractères
auth-error-email-invalid = Veuillez entrer une adresse email valide
## Chat page
chat-no-room = Sélectionnez un salon pour commencer à discuter
chat-input-placeholder = tapez un message
chat-invite-title = Inviter des gens
chat-invite-receiver = Nom d'utilisateur
chat-invite-friend-too = Envoyer aussi une demande d'ami
chat-invite-send = Envoyer
chat-room-list-title = Salons
chat-room-owner = par {$owner}
chat-create-title = Créer un salon
chat-create-name-placeholder = Nom du salon
chat-create-global = Salon public
chat-create-submit = Créer
## Friends page
friends-title = Vos amis
friends-add-title = Ajouter un ami
friends-send-request = Envoyer
friends-list-header = Vos Amis
friends-error-required = Le nom d'utilisateur est requis.
## Notifications page
notifications-title = Notifications
notifications-friend-requests = Demandes d'amis
notifications-room-invites = Invitations
notifications-no-requests = Aucune demande en attente
notifications-no-invites = Aucune invitation en attente
notifications-accept = Accepter
notifications-join = Rejoindre
notifications-invite-from = de : {$user}
notifications-error-friend = Erreur lors de l'acceptation de la demande.
notifications-error-room = Erreur lors de l'acceptation de l'invitation.
## Account page
account-title = Votre Compte
account-label-username = Nom d'utilisateur :
account-label-email = Email :
account-update-btn = Modifier
account-logout-btn = Déconnexion
account-loading = Chargement du compte...
account-update-modal-title = Modifier votre compte
account-update-subtitle = Remplissez uniquement ce que vous souhaitez changer.
account-new-password = Nouveau mot de passe
account-new-password-confirm = Confirmer le mot de passe
account-update-save = Enregistrer
account-updating = Mise à jour...
account-language = Langue
account-error-required = Le nom d'utilisateur et l'email sont requis.
account-error-failed = Échec de la mise à jour
## Shared
shared-cancel = Annuler
shared-error = Une erreur est survenue

View File

@@ -2,14 +2,27 @@ import { createApp } from 'vue'
import router from './router.ts'
import App from './App.vue'
import { validateToken } from './store.ts'
import { fluent, setLanguage } from './i18n'
import './base.css'
import { getLocalePreference } from './authStore.ts'
async function init() {
await validateToken()
const app = createApp(App)
app.use(router)
app.use(fluent)
const savedLocale = await getLocalePreference();
const osLocale = navigator.language;
if (savedLocale) {
setLanguage(savedLocale);
} else {
setLanguage(osLocale);
}
app.mount('#app')
}

View File

@@ -1,36 +1,53 @@
<template>
<div class="account-page">
<h1>Your Account</h1>
<h1>{{ $t('account-title') }}</h1>
<UpdateAccountModal v-if="showUpdateModal" :user="user" @close="showUpdateModal = false" @updated="fetchUserData" />
<div v-if="user" class="info-card">
<button class="update-btn" @click="showUpdateModal = true">Update</button>
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Email:</strong> {{ user.email }}</p>
<button class="update-btn" @click="showUpdateModal = true">{{ $t('account-update-btn') }}</button>
<p><strong>{{ $t('account-label-username') }}</strong> {{ user.username }}</p>
<p><strong>{{ $t('account-label-email') }}</strong> {{ user.email }}</p>
</div>
<div v-else class="loading-state">
<p>Loading account details...</p>
<p>{{ $t('account-loading') }}</p>
</div>
<div class="input-group">
<span>{{ $t('account-language') }}</span>
<div class="lang-grid">
<button v-for="lang in languages" :key="lang.code" class="lang-btn"
:class="{ active: currentLang === lang.code }" @click="changeLanguage(lang.code)">
{{ lang.name }}
</button>
</div>
</div>
<button class="logout-btn" @click="logout">
<i class="fa-solid fa-right-from-bracket"></i>
<span>Logout</span>
<span>{{ $t('account-logout-btn') }}</span>
</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { logout as authLogout } from '../store.ts'
import { getAuthData } from "../authStore.ts"
import type { User } from "../types"
import UpdateAccountModal from '../components/UpdateAccountModal.vue'
import { useFluent } from 'fluent-vue'
import { saveLocalePreference, getLocalePreference } from "../authStore.ts"
import { getSupportedLanguagesMetadata, setLanguage } from '../i18n'
const router = useRouter()
const user = ref<User | null>(null)
const showUpdateModal = ref(false)
const { $t } = useFluent()
const currentLang = ref('')
const languages = computed(() => getSupportedLanguagesMetadata())
async function fetchUserData() {
try {
@@ -41,7 +58,19 @@ async function fetchUserData() {
}
}
onMounted(fetchUserData)
onMounted(async () => {
const pref = await getLocalePreference()
// Synchronize the UI state with the actual active language
currentLang.value = pref || (navigator.language.split('-')[0])
fetchUserData()
})
async function changeLanguage(code: string) {
const actual = setLanguage(code)
currentLang.value = actual
await saveLocalePreference(actual)
}
function logout() {
authLogout()
@@ -73,6 +102,26 @@ function logout() {
top: 10px;
}
.lang-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 0.5rem;
margin-top: 0.5rem;
}
.lang-btn {
background: var(--panel);
border: 1px solid var(--border);
color: var(--text);
margin: 0;
}
.lang-btn.active {
background: var(--accent);
color: #000;
border-color: var(--accent);
}
.logout-btn {
cursor: pointer;
display: flex;

View File

@@ -1,21 +1,20 @@
<template>
<div class="friends-page">
<header class="friends-header">
<h1>Your friends</h1>
<h1>{{ $t('friends-title') }}</h1>
<form class="friend-request-form" @submit.prevent="send">
<h3>Add Friend</h3>
<h3>{{ $t('friends-add-title') }}</h3>
<div class="input-container">
<input v-model="username" placeholder="Username" autofocus />
<button type="submit">Send Request</button>
<input v-model="username" :placeholder="$t('auth-username')" autofocus />
<button type="submit">{{ $t('friends-send-request') }}</button>
</div>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
</form>
</header>
<!-- Friends List -->
<div class="friends-list">
<h2>Your Friends</h2>
<h2>{{ $t('friends-list-header') }}</h2>
<ul>
<li v-for="friend in friends" :key="friend.uuid">
{{ friend.username }}

View File

@@ -1,7 +1,29 @@
<template>
<div class="login-page">
<form class="login-card" @submit.prevent="submit">
<h1>{{ $t('auth-login-title') }}</h1>
<div class="input-group">
<label>{{ $t('auth-email') }}</label>
<input v-model="email" :placeholder="$t('auth-email').toLowerCase()" />
</div>
<div class="input-group">
<label>{{ $t('auth-password') }}</label>
<input v-model="password" type="password" :placeholder="$t('auth-password').toLowerCase()" />
</div>
<button type="submit">{{ $t('auth-login-btn') }}</button>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<p class="register-link">
{{ $t('auth-no-account') }} <router-link to="/register">{{ $t('auth-register-title') }}</router-link>
</p>
</form>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { login } from '../store.ts'
import { useRouter } from "vue-router";
import { useFluent } from 'fluent-vue';
const email = ref("");
const password = ref("");
@@ -9,85 +31,61 @@ const errorMessage = ref("");
const router = useRouter();
const { $t } = useFluent();
async function submit() {
errorMessage.value = "";
try {
await login(email.value, "", password.value);
router.push("/");
} catch (err: any) {
errorMessage.value = err?.message || "An unknown error occurred";
}
errorMessage.value = "";
try {
await login(email.value, "", password.value);
router.push("/");
} catch (err: any) {
errorMessage.value = err?.message || $t('auth-error-unknown');
}
}
</script>
<template>
<div class="login-page">
<form class="login-card" @submit.prevent="submit">
<h1>Login</h1>
<div class="input-group">
<label>Email</label>
<input v-model="email" placeholder="email" />
</div>
<div class="input-group">
<label>Password</label>
<input v-model="password" type="password" placeholder="password" />
</div>
<button type="submit">Login</button>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<p class="register-link">
Don't have an account? <router-link to="/register">Register</router-link>
</p>
</form>
</div>
</template>
<style scoped>
.login-page {
height: 100%;
display: grid;
place-items: center;
height: 100%;
display: grid;
place-items: center;
}
.login-card {
width: 100%;
max-width: 360px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
width: 100%;
max-width: 360px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.login-card h1 {
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 0.5rem;
text-align: center;
font-size: 1.4rem;
font-weight: 600;
margin-bottom: 0.5rem;
text-align: center;
}
.register-link {
font-size: 0.9rem;
text-align: center;
color: var(--text-muted);
font-size: 0.9rem;
text-align: center;
color: var(--text-muted);
}
.register-link a {
color: var(--accent);
margin-left: 5px;
color: var(--accent);
margin-left: 5px;
}
.error-message {
color: var(--error);
font-size: 0.9rem;
text-align: center;
margin-top: 0.5rem;
color: var(--error);
font-size: 0.9rem;
text-align: center;
margin-top: 0.5rem;
}
</style>

View File

@@ -1,32 +1,31 @@
<template>
<div class="notifications-page">
<h1>Notifications</h1>
<h1>{{ $t('notifications-title') }}</h1>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
<!-- Friend Request List -->
<div class="list">
<h2>Friend Requests</h2>
<h2>{{ $t('notifications-friend-requests') }}</h2>
<ul v-if="requests.length">
<li v-for="req in requests" :key="req.sender_uuid">
<span>{{ req.sender_username }}</span>
<button @click="acceptFriend(req.sender_uuid)">Accept</button>
<button @click="acceptFriend(req.sender_uuid)">{{ $t('notifications-accept') }}</button>
</li>
</ul>
<p v-else>No pending requests</p>
<p v-else>{{ $t('notifications-no-requests') }}</p>
</div>
<!-- Room Invites List -->
<div class="list">
<h2>Room Invites</h2>
<h2>{{ $t('notifications-room-invites') }}</h2>
<ul v-if="invites.length">
<li v-for="inv in invites" :key="inv.sender_uuid">
<span>{{ inv.room_name }}</span>
<span>from: {{ inv.sender_username }}</span>
<button @click="acceptRoom(inv.sender_uuid, inv.room_uuid)">Join</button>
<span>{{ $t('notifications-invite-from', { user: inv.sender_username }) }}</span>
<button @click="acceptRoom(inv.sender_uuid, inv.room_uuid)">{{ $t('notifications-join') }}</button>
</li>
</ul>
<p v-else>No pending invites</p>
<p v-else>{{ $t('notifications-no-invites') }}</p>
</div>
</div>
</template>
@@ -36,6 +35,9 @@ import { onMounted, ref } from 'vue'
import { fetchFriendRequests, acceptFriendRequest } from '../api/friends'
import { fetchRoomInvites, acceptRoomInvite } from '../api/rooms.ts'
import { useNotifications } from '../store'
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const errorMessage = ref('')
const { requests, invites, refreshNotifications } = useNotifications()
@@ -52,7 +54,7 @@ async function acceptFriend(senderUuid: string) {
requests.value = requests.value.filter(r => r.sender_uuid !== senderUuid)
// fetchFriends().then(f => (friends.value = f))
} catch (err) {
errorMessage.value = 'An error occurred while accepting the request.' // TODO: handle this case
errorMessage.value = $t('notifications-error-friend') // TODO: handle this case
}
}
@@ -62,7 +64,7 @@ async function acceptRoom(senderUuid: string, roomUuid: string) {
invites.value = invites.value.filter(r => r.room_uuid !== roomUuid)
// fetchFriends().then(f => (friends.value = f))
} catch (err) {
errorMessage.value = 'An error occurred while accepting the invite.' // TODO: handle this case
errorMessage.value = $t('notifications-error-room') // TODO: handle this case
throw err
}
}

View File

@@ -1,28 +1,28 @@
<template>
<div class="login-page">
<form class="login-card" @submit.prevent="submit" novalidate>
<h1>Register</h1>
<h1>{{ $t('auth-register-title') }}</h1>
<div class="input-group">
<label>Email</label>
<input v-model="email" type="email" placeholder="email" required />
<label>{{ $t('auth-email') }}</label>
<input v-model="email" type="email" :placeholder="$t('auth-email').toLowerCase()" required />
</div>
<div class="input-group">
<label>Username</label>
<input v-model="username" placeholder="username" required />
<label>{{ $t('auth-username') }}</label>
<input v-model="username" :placeholder="$t('auth-username').toLowerCase()" required />
</div>
<div class="input-group">
<label>Password</label>
<input v-model="password" type="password" placeholder="password" required />
<input v-model="confirmPassword" type="password" placeholder="confirm password" required />
<label>{{ $t('auth-password') }}</label>
<input v-model="password" type="password" :placeholder="$t('auth-password').toLowerCase()" required />
<input v-model="confirmPassword" type="password" :placeholder="$t('auth-confirm-password')" required />
</div>
<button type="submit">Create Account</button>
<button type="submit">{{ $t('auth-register-btn') }}</button>
<p class="login-link">
Already have an account? <router-link to="/login">Login</router-link>
{{ $t('auth-has-account') }} <router-link to="/login">{{ $t('auth-login-title') }}</router-link>
</p>
<p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
@@ -34,6 +34,9 @@
import { ref } from "vue";
import { register } from '../store.ts'
import { useRouter } from "vue-router";
import { useFluent } from 'fluent-vue';
const { $t } = useFluent();
const email = ref("");
const username = ref("");
@@ -51,16 +54,16 @@ async function submit(event: Event) {
// Check password length and email
if (!form.checkValidity()) {
if (password.value.length < 8) {
errorMessage.value = "Password must be at least 8 characters long";
errorMessage.value = $t('auth-error-password-length');
} else {
errorMessage.value = "Please enter a valid email address";
errorMessage.value = $t('auth-error-email-invalid');
}
return;
}
// Check password match
if (password.value !== confirmPassword.value) {
errorMessage.value = "Passwords do not match";
errorMessage.value = $t('auth-error-password-match');
return;
}