Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 52 additions & 13 deletions src/components/pages/LanguagesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useEffect, useState } from "react"
import { Languages, Plus } from "lucide-react"
import { Languages, Plus, Pencil } from "lucide-react"
import { toast } from "sonner"
import { languagesAPI } from "@/services/api"
import type { LanguageResponse } from "@/types"
import { useLanguagesStore } from "@/stores/languagesStore"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
Expand All @@ -17,14 +18,17 @@ import {
import { LoadingSpinner } from "@/components/common/LoadingSpinner"
import { EmptyState } from "@/components/common/EmptyState"
import { InfoTooltip } from "@/components/common/InfoTooltip"
import { useAuth } from "@/contexts/AuthContext"

import { formatDate } from "@/utils/format"

export default function LanguagesPage() {
const { isPlatformAdmin } = useAuth()
const { languages, loading: storeLoading, lastFetched, fetch: fetchLanguages } = useLanguagesStore()
const loading = storeLoading || (!lastFetched && languages.length === 0)
const [dialogOpen, setDialogOpen] = useState(false)
const [creating, setCreating] = useState(false)
const [editingLang, setEditingLang] = useState<LanguageResponse | null>(null)
const [name, setName] = useState("")
const [code, setCode] = useState("")

Expand All @@ -33,18 +37,34 @@ export default function LanguagesPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

function openDialog() {
function openCreateDialog() {
setEditingLang(null)
setName("")
setCode("")
setDialogOpen(true)
}

async function handleCreate() {
function openEditDialog(lang: LanguageResponse) {
setEditingLang(lang)
setName(lang.name)
setCode(lang.code)
setDialogOpen(true)
}

async function handleSave() {
if (!name.trim() || code.trim().length !== 3) return
setCreating(true)
try {
await languagesAPI.create({ name: name.trim(), code: code.trim().toLowerCase() })
toast.success("Language created")
if (editingLang) {
await languagesAPI.update(editingLang.id, {
name: name.trim(),
code: code.trim().toLowerCase(),
})
toast.success("Language updated")
} else {
await languagesAPI.create({ name: name.trim(), code: code.trim().toLowerCase() })
toast.success("Language created")
}
setDialogOpen(false)
useLanguagesStore.getState().invalidate()
await fetchLanguages()
Expand All @@ -53,7 +73,7 @@ export default function LanguagesPage() {
if (status === 409) {
toast.error("A language with this code already exists")
} else {
toast.error("Failed to create language")
toast.error(editingLang ? "Failed to update language" : "Failed to create language")
}
} finally {
setCreating(false)
Expand All @@ -76,7 +96,7 @@ export default function LanguagesPage() {
{languages.length} language{languages.length !== 1 ? "s" : ""} registered
</p>
</div>
<Button onClick={openDialog} className="rounded-xl">
<Button onClick={openCreateDialog} className="rounded-xl">
<Plus className="h-4 w-4" />
New Language
</Button>
Expand All @@ -88,15 +108,26 @@ export default function LanguagesPage() {
title="No languages yet"
description="Languages define the translation targets for your projects. Create one to get started."
actionLabel="Create Language"
onAction={openDialog}
onAction={openCreateDialog}
/>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{languages.map((lang) => (
<div
key={lang.id}
className="group rounded-2xl border border-areia/20 bg-surface p-5 shadow-sm hover:shadow-md hover:border-telha/30 transition-all duration-200 cursor-default"
className="group relative rounded-2xl border border-areia/20 bg-surface p-5 shadow-sm hover:shadow-md hover:border-telha/30 transition-all duration-200 cursor-default"
>
{isPlatformAdmin && (
<Button
variant="ghost"
size="sm"
className="absolute top-2 right-2 h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => openEditDialog(lang)}
aria-label={`Edit ${lang.name}`}
>
<Pencil className="h-3.5 w-3.5" />
</Button>
)}
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-azul/15 to-azul/5 flex items-center justify-center mx-auto">
<span className="text-2xl font-mono font-bold text-azul">
{lang.code}
Expand All @@ -116,9 +147,11 @@ export default function LanguagesPage() {
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Create Language</DialogTitle>
<DialogTitle>{editingLang ? "Edit Language" : "Create Language"}</DialogTitle>
<DialogDescription>
Add a new target language for your translation projects.
{editingLang
? "Update the language name or code."
: "Add a new target language for your translation projects."}
</DialogDescription>
</DialogHeader>
<div className="space-y-5 pt-1">
Expand Down Expand Up @@ -159,10 +192,16 @@ export default function LanguagesPage() {
Cancel
</Button>
<Button
onClick={handleCreate}
onClick={handleSave}
disabled={creating || !name.trim() || code.trim().length !== 3}
>
{creating ? "Creating..." : "Create"}
{creating
? editingLang
? "Saving..."
: "Creating..."
: editingLang
? "Save Changes"
: "Create"}
</Button>
</DialogFooter>
</DialogContent>
Expand Down
3 changes: 3 additions & 0 deletions src/services/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
AppRoleResponse,
LanguageResponse,
LanguageCreate,
LanguageUpdate,
OrganizationResponse,
OrganizationCreate,
OrganizationUpdate,
Expand Down Expand Up @@ -170,6 +171,8 @@ export const languagesAPI = {
list: () => api.get<LanguageResponse[]>("/languages"),
create: (data: LanguageCreate) =>
api.post<LanguageResponse>("/languages", data),
update: (languageId: string, data: LanguageUpdate) =>
api.put<LanguageResponse>(`/languages/${languageId}`, data),
get: (languageId: string) =>
api.get<LanguageResponse>(`/languages/${languageId}`),
getByCode: (code: string) =>
Expand Down
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type {
export type {
LanguageResponse,
LanguageCreate,
LanguageUpdate,
} from "./language"

export type {
Expand Down
6 changes: 6 additions & 0 deletions src/types/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ export interface LanguageResponse {
name: string
code: string
created_at: string
updated_at: string
}

export interface LanguageCreate {
name: string
code: string
}

export interface LanguageUpdate {
name?: string
code?: string
}
Loading