feat: change language
This commit is contained in:
@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
|
|||||||
|
|
||||||
import { CopyInviteButton } from "@/components/admin/copy-invite-button"
|
import { CopyInviteButton } from "@/components/admin/copy-invite-button"
|
||||||
import { DeleteInvitationDialog } from "@/components/admin/delete-invitation-dialog"
|
import { DeleteInvitationDialog } from "@/components/admin/delete-invitation-dialog"
|
||||||
|
import { InvitationLanguageSelect } from "@/components/admin/invitation-language-select"
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -146,6 +147,14 @@ export default async function GuestDetailPage({
|
|||||||
<CardDescription>Admin-managed invitation fields.</CardDescription>
|
<CardDescription>Admin-managed invitation fields.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 text-sm">
|
<CardContent className="space-y-3 text-sm">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<span className="text-muted-foreground">Language</span>
|
||||||
|
<InvitationLanguageSelect
|
||||||
|
invitationId={detail.invitation.id}
|
||||||
|
currentLanguage={detail.invitation.language}
|
||||||
|
displayName={detail.invitation.display_name}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between gap-4">
|
<div className="flex justify-between gap-4">
|
||||||
<span className="text-muted-foreground">Contact email</span>
|
<span className="text-muted-foreground">Contact email</span>
|
||||||
<span className="text-right">{detail.invitation.contact_email ?? "—"}</span>
|
<span className="text-right">{detail.invitation.contact_email ?? "—"}</span>
|
||||||
|
|||||||
@@ -4,14 +4,25 @@ import { revalidatePath } from "next/cache"
|
|||||||
|
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
import { isAdminEmail } from "@/lib/admin-auth"
|
import { isAdminEmail } from "@/lib/admin-auth"
|
||||||
import { updateSaveTheDateSent } from "@/lib/db/queries/invitations"
|
import {
|
||||||
import { updateSaveTheDateSentSchema } from "@/lib/validation/invitation"
|
updateInvitationLanguage,
|
||||||
|
updateSaveTheDateSent,
|
||||||
|
} from "@/lib/db/queries/invitations"
|
||||||
|
import {
|
||||||
|
updateInvitationLanguageSchema,
|
||||||
|
updateSaveTheDateSentSchema,
|
||||||
|
} from "@/lib/validation/invitation"
|
||||||
|
|
||||||
export interface UpdateSaveTheDateSentResult {
|
export interface UpdateSaveTheDateSentResult {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
error: string | null
|
error: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UpdateInvitationLanguageResult {
|
||||||
|
ok: boolean
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateSaveTheDateSentAction(input: {
|
export async function updateSaveTheDateSentAction(input: {
|
||||||
invitationId: string
|
invitationId: string
|
||||||
sent: boolean
|
sent: boolean
|
||||||
@@ -49,3 +60,42 @@ export async function updateSaveTheDateSentAction(input: {
|
|||||||
|
|
||||||
return { ok: true, error: null }
|
return { ok: true, error: null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateInvitationLanguageAction(input: {
|
||||||
|
invitationId: string
|
||||||
|
language: string
|
||||||
|
}): Promise<UpdateInvitationLanguageResult> {
|
||||||
|
const session = await auth()
|
||||||
|
const adminEmail = session?.user?.email
|
||||||
|
|
||||||
|
if (!isAdminEmail(adminEmail)) {
|
||||||
|
return { ok: false, error: "You are not allowed to update invitations." }
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = updateInvitationLanguageSchema.safeParse(input)
|
||||||
|
|
||||||
|
if (!parsed.success) {
|
||||||
|
return { ok: false, error: "Select a supported language." }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updated = await updateInvitationLanguage({
|
||||||
|
invitationId: parsed.data.invitationId,
|
||||||
|
language: parsed.data.language,
|
||||||
|
adminEmail: adminEmail ?? "",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!updated) {
|
||||||
|
return { ok: false, error: "This guest group no longer exists." }
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update invitation language", error)
|
||||||
|
return { ok: false, error: "Could not update the invitation language." }
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath(`/admin/guests/${parsed.data.invitationId}`)
|
||||||
|
revalidatePath("/admin/guests")
|
||||||
|
revalidatePath("/admin")
|
||||||
|
|
||||||
|
return { ok: true, error: null }
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation"
|
||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
import { updateInvitationLanguageAction } from "@/app/admin/guests/actions"
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select"
|
||||||
|
import { supportedLanguages } from "@/lib/i18n/config"
|
||||||
|
|
||||||
|
interface InvitationLanguageSelectProps {
|
||||||
|
invitationId: string
|
||||||
|
currentLanguage: string
|
||||||
|
displayName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InvitationLanguageSelect({
|
||||||
|
invitationId,
|
||||||
|
currentLanguage,
|
||||||
|
displayName,
|
||||||
|
}: InvitationLanguageSelectProps) {
|
||||||
|
const router = useRouter()
|
||||||
|
const [value, setValue] = useState(currentLanguage)
|
||||||
|
const [message, setMessage] = useState<string | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [isSaving, setIsSaving] = useState(false)
|
||||||
|
|
||||||
|
async function updateLanguage(nextLanguage: string) {
|
||||||
|
if (nextLanguage === value) return
|
||||||
|
|
||||||
|
const previousLanguage = value
|
||||||
|
|
||||||
|
setValue(nextLanguage)
|
||||||
|
setMessage(null)
|
||||||
|
setError(null)
|
||||||
|
setIsSaving(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await updateInvitationLanguageAction({
|
||||||
|
invitationId,
|
||||||
|
language: nextLanguage,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
setValue(previousLanguage)
|
||||||
|
setError(result.error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessage("Language updated.")
|
||||||
|
router.refresh()
|
||||||
|
} catch {
|
||||||
|
setValue(previousLanguage)
|
||||||
|
setError("Could not update the invitation language.")
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
disabled={isSaving}
|
||||||
|
onValueChange={(next) => void updateLanguage(next)}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
aria-label={`Invitation language for ${displayName}`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Select language" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{supportedLanguages.map((language) => (
|
||||||
|
<SelectItem key={language.code} value={language.code}>
|
||||||
|
{language.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
{message ? <p className="text-xs text-emerald-700">{message}</p> : null}
|
||||||
|
{error ? <p className="text-xs text-destructive">{error}</p> : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -191,6 +191,55 @@ export async function updateSaveTheDateSent({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateInvitationLanguage({
|
||||||
|
invitationId,
|
||||||
|
language,
|
||||||
|
adminEmail,
|
||||||
|
}: {
|
||||||
|
invitationId: string
|
||||||
|
language: string
|
||||||
|
adminEmail: string
|
||||||
|
}) {
|
||||||
|
return db.transaction().execute(async (trx) => {
|
||||||
|
const invitation = await trx
|
||||||
|
.selectFrom("invitations")
|
||||||
|
.select(["id", "display_name", "language"])
|
||||||
|
.where("id", "=", invitationId)
|
||||||
|
.executeTakeFirst()
|
||||||
|
|
||||||
|
if (!invitation) return null
|
||||||
|
|
||||||
|
const updated = await trx
|
||||||
|
.updateTable("invitations")
|
||||||
|
.set({
|
||||||
|
language,
|
||||||
|
updated_at: new Date(),
|
||||||
|
})
|
||||||
|
.where("id", "=", invitationId)
|
||||||
|
.returning(["id", "language"])
|
||||||
|
.executeTakeFirst()
|
||||||
|
|
||||||
|
if (!updated) return null
|
||||||
|
|
||||||
|
await trx
|
||||||
|
.insertInto("admin_audit_events")
|
||||||
|
.values({
|
||||||
|
admin_email: adminEmail,
|
||||||
|
action: "invitation.language_updated",
|
||||||
|
target_type: "invitation",
|
||||||
|
target_id: invitationId,
|
||||||
|
metadata: {
|
||||||
|
displayName: invitation.display_name,
|
||||||
|
previousLanguage: invitation.language,
|
||||||
|
newLanguage: language,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.execute()
|
||||||
|
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function listInvitations(): Promise<InvitationListItem[]> {
|
export async function listInvitations(): Promise<InvitationListItem[]> {
|
||||||
const invitations = await db
|
const invitations = await db
|
||||||
.selectFrom("invitations")
|
.selectFrom("invitations")
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ export const updateSaveTheDateSentSchema = z.object({
|
|||||||
sent: z.boolean(),
|
sent: z.boolean(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const updateInvitationLanguageSchema = z.object({
|
||||||
|
invitationId: z.uuid(),
|
||||||
|
language: z.enum(supportedLanguageCodes, {
|
||||||
|
error: "Select a supported language",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
export type CreateInvitationInput = z.infer<typeof createInvitationSchema>
|
export type CreateInvitationInput = z.infer<typeof createInvitationSchema>
|
||||||
export type DeleteInvitationInput = z.infer<typeof deleteInvitationSchema>
|
export type DeleteInvitationInput = z.infer<typeof deleteInvitationSchema>
|
||||||
export type UpdateSaveTheDateSentInput = z.infer<typeof updateSaveTheDateSentSchema>
|
export type UpdateSaveTheDateSentInput = z.infer<typeof updateSaveTheDateSentSchema>
|
||||||
|
export type UpdateInvitationLanguageInput = z.infer<typeof updateInvitationLanguageSchema>
|
||||||
|
|||||||
Reference in New Issue
Block a user