feat(i18n): add verification messages in multiple languages

This commit is contained in:
Fatih Kadir Akın
2025-12-12 13:50:47 +03:00
parent 731bcf0f7d
commit 7c94ece179
11 changed files with 79 additions and 8 deletions
+5
View File
@@ -271,6 +271,11 @@
"deleteFailed": "فشل حذف المستخدم", "deleteFailed": "فشل حذف المستخدم",
"roleUpdated": "تم تحديث دور المستخدم", "roleUpdated": "تم تحديث دور المستخدم",
"roleUpdateFailed": "فشل تحديث الدور", "roleUpdateFailed": "فشل تحديث الدور",
"verify": "توثيق",
"unverify": "إلغاء التوثيق",
"verified": "تم توثيق المستخدم",
"unverified": "تم إلغاء توثيق المستخدم",
"verifyFailed": "فشل تحديث التوثيق",
"deleteConfirmTitle": "حذف المستخدم؟", "deleteConfirmTitle": "حذف المستخدم؟",
"deleteConfirmDescription": "هذا الإجراء لا يمكن التراجع عنه. سيتم حذف جميع بيانات المستخدم نهائياً." "deleteConfirmDescription": "هذا الإجراء لا يمكن التراجع عنه. سيتم حذف جميع بيانات المستخدم نهائياً."
}, },
+5
View File
@@ -305,6 +305,11 @@
"deleteFailed": "Failed to delete user", "deleteFailed": "Failed to delete user",
"roleUpdated": "User role updated", "roleUpdated": "User role updated",
"roleUpdateFailed": "Failed to update role", "roleUpdateFailed": "Failed to update role",
"verify": "Verify",
"unverify": "Unverify",
"verified": "User verified",
"unverified": "User unverified",
"verifyFailed": "Failed to update verification",
"deleteConfirmTitle": "Delete User?", "deleteConfirmTitle": "Delete User?",
"deleteConfirmDescription": "This action cannot be undone. All user data will be permanently deleted." "deleteConfirmDescription": "This action cannot be undone. All user data will be permanently deleted."
}, },
+5
View File
@@ -265,6 +265,11 @@
"deleteFailed": "Error al eliminar usuario", "deleteFailed": "Error al eliminar usuario",
"roleUpdated": "Rol de usuario actualizado", "roleUpdated": "Rol de usuario actualizado",
"roleUpdateFailed": "Error al actualizar rol", "roleUpdateFailed": "Error al actualizar rol",
"verify": "Verificar",
"unverify": "Quitar verificación",
"verified": "Usuario verificado",
"unverified": "Usuario no verificado",
"verifyFailed": "Error al actualizar verificación",
"deleteConfirmTitle": "¿Eliminar Usuario?", "deleteConfirmTitle": "¿Eliminar Usuario?",
"deleteConfirmDescription": "Esta acción no se puede deshacer. Todos los datos del usuario serán eliminados permanentemente." "deleteConfirmDescription": "Esta acción no se puede deshacer. Todos los datos del usuario serán eliminados permanentemente."
}, },
+5
View File
@@ -265,6 +265,11 @@
"deleteFailed": "ユーザーの削除に失敗しました", "deleteFailed": "ユーザーの削除に失敗しました",
"roleUpdated": "ユーザーの役割を更新しました", "roleUpdated": "ユーザーの役割を更新しました",
"roleUpdateFailed": "役割の更新に失敗しました", "roleUpdateFailed": "役割の更新に失敗しました",
"verify": "認証",
"unverify": "認証解除",
"verified": "ユーザーを認証しました",
"unverified": "ユーザーの認証を解除しました",
"verifyFailed": "認証の更新に失敗しました",
"deleteConfirmTitle": "ユーザーを削除しますか?", "deleteConfirmTitle": "ユーザーを削除しますか?",
"deleteConfirmDescription": "この操作は取り消せません。すべてのユーザーデータが完全に削除されます。" "deleteConfirmDescription": "この操作は取り消せません。すべてのユーザーデータが完全に削除されます。"
}, },
+5
View File
@@ -305,6 +305,11 @@
"deleteFailed": "Kullanıcı silinemedi", "deleteFailed": "Kullanıcı silinemedi",
"roleUpdated": "Kullanıcı rolü güncellendi", "roleUpdated": "Kullanıcı rolü güncellendi",
"roleUpdateFailed": "Rol güncellenemedi", "roleUpdateFailed": "Rol güncellenemedi",
"verify": "Doğrula",
"unverify": "Doğrulamayı Kaldır",
"verified": "Kullanıcı doğrulandı",
"unverified": "Kullanıcı doğrulaması kaldırıldı",
"verifyFailed": "Doğrulama güncellenemedi",
"deleteConfirmTitle": "Kullanıcı Silinsin mi?", "deleteConfirmTitle": "Kullanıcı Silinsin mi?",
"deleteConfirmDescription": "Bu işlem geri alınamaz. Tüm kullanıcı verileri kalıcı olarak silinecektir." "deleteConfirmDescription": "Bu işlem geri alınamaz. Tüm kullanıcı verileri kalıcı olarak silinecektir."
}, },
+5
View File
@@ -265,6 +265,11 @@
"deleteFailed": "删除用户失败", "deleteFailed": "删除用户失败",
"roleUpdated": "用户角色已更新", "roleUpdated": "用户角色已更新",
"roleUpdateFailed": "更新角色失败", "roleUpdateFailed": "更新角色失败",
"verify": "验证",
"unverify": "取消验证",
"verified": "用户已验证",
"unverified": "用户验证已取消",
"verifyFailed": "更新验证失败",
"deleteConfirmTitle": "删除用户?", "deleteConfirmTitle": "删除用户?",
"deleteConfirmDescription": "此操作无法撤销。所有用户数据将被永久删除。" "deleteConfirmDescription": "此操作无法撤销。所有用户数据将被永久删除。"
}, },
+1
View File
@@ -28,6 +28,7 @@ model User {
avatar String? avatar String?
role UserRole @default(USER) role UserRole @default(USER)
locale String @default("en") locale String @default("en")
verified Boolean @default(false)
emailVerified DateTime? emailVerified DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
+5 -1
View File
@@ -3,7 +3,7 @@ import { notFound } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import { getTranslations, getLocale } from "next-intl/server"; import { getTranslations, getLocale } from "next-intl/server";
import { formatDistanceToNow } from "@/lib/date"; import { formatDistanceToNow } from "@/lib/date";
import { Calendar, ArrowBigUp, FileText, Settings, GitPullRequest, Clock, Check, X, Pin } from "lucide-react"; import { Calendar, ArrowBigUp, FileText, Settings, GitPullRequest, Clock, Check, X, Pin, BadgeCheck } from "lucide-react";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@@ -75,6 +75,7 @@ export default async function UserProfilePage({ params, searchParams }: UserProf
email: true, email: true,
avatar: true, avatar: true,
role: true, role: true,
verified: true,
createdAt: true, createdAt: true,
_count: { _count: {
select: { select: {
@@ -283,6 +284,9 @@ export default async function UserProfilePage({ params, searchParams }: UserProf
<div> <div>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<h1 className="text-2xl font-bold">{user.name || user.username}</h1> <h1 className="text-2xl font-bold">{user.name || user.username}</h1>
{user.verified && (
<BadgeCheck className="h-5 w-5 text-blue-500" />
)}
{user.role === "ADMIN" && ( {user.role === "ADMIN" && (
<Badge variant="default" className="text-xs">Admin</Badge> <Badge variant="default" className="text-xs">Admin</Badge>
)} )}
+1
View File
@@ -59,6 +59,7 @@ export default async function AdminPage() {
name: true, name: true,
avatar: true, avatar: true,
role: true, role: true,
verified: true,
createdAt: true, createdAt: true,
_count: { _count: {
select: { select: {
+15 -5
View File
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { db } from "@/lib/db"; import { db } from "@/lib/db";
// Update user (role change) // Update user (role change or verification)
export async function PATCH( export async function PATCH(
request: NextRequest, request: NextRequest,
{ params }: { params: Promise<{ id: string }> } { params }: { params: Promise<{ id: string }> }
@@ -15,15 +15,25 @@ export async function PATCH(
const { id } = await params; const { id } = await params;
const body = await request.json(); const body = await request.json();
const { role } = body; const { role, verified } = body;
if (!["ADMIN", "USER"].includes(role)) { // Build update data
return NextResponse.json({ error: "Invalid role" }, { status: 400 }); const updateData: { role?: "ADMIN" | "USER"; verified?: boolean } = {};
if (role !== undefined) {
if (!["ADMIN", "USER"].includes(role)) {
return NextResponse.json({ error: "Invalid role" }, { status: 400 });
}
updateData.role = role;
}
if (verified !== undefined) {
updateData.verified = verified;
} }
const user = await db.user.update({ const user = await db.user.update({
where: { id }, where: { id },
data: { role }, data: updateData,
}); });
return NextResponse.json(user); return NextResponse.json(user);
+27 -2
View File
@@ -4,7 +4,7 @@ import { useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useTranslations, useLocale } from "next-intl"; import { useTranslations, useLocale } from "next-intl";
import { formatDistanceToNow } from "@/lib/date"; import { formatDistanceToNow } from "@/lib/date";
import { MoreHorizontal, Shield, User, Trash2 } from "lucide-react"; import { MoreHorizontal, Shield, User, Trash2, BadgeCheck } from "lucide-react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -42,6 +42,7 @@ interface User {
name: string | null; name: string | null;
avatar: string | null; avatar: string | null;
role: "ADMIN" | "USER"; role: "ADMIN" | "USER";
verified: boolean;
createdAt: Date; createdAt: Date;
_count: { _count: {
prompts: number; prompts: number;
@@ -76,6 +77,23 @@ export function UsersTable({ users }: UsersTableProps) {
} }
}; };
const handleVerifyToggle = async (userId: string, verified: boolean) => {
try {
const res = await fetch(`/api/admin/users/${userId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ verified }),
});
if (!res.ok) throw new Error("Failed to update verification");
toast.success(verified ? t("verified") : t("unverified"));
router.refresh();
} catch {
toast.error(t("verifyFailed"));
}
};
const handleDelete = async () => { const handleDelete = async () => {
if (!deleteUserId) return; if (!deleteUserId) return;
@@ -130,7 +148,10 @@ export function UsersTable({ users }: UsersTableProps) {
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div> <div>
<div className="font-medium">{user.name || user.username}</div> <div className="font-medium flex items-center gap-1">
{user.name || user.username}
{user.verified && <BadgeCheck className="h-4 w-4 text-blue-500" />}
</div>
<div className="text-xs text-muted-foreground">@{user.username}</div> <div className="text-xs text-muted-foreground">@{user.username}</div>
</div> </div>
</div> </div>
@@ -165,6 +186,10 @@ export function UsersTable({ users }: UsersTableProps) {
{t("removeAdmin")} {t("removeAdmin")}
</DropdownMenuItem> </DropdownMenuItem>
)} )}
<DropdownMenuItem onClick={() => handleVerifyToggle(user.id, !user.verified)}>
<BadgeCheck className="h-4 w-4 mr-2" />
{user.verified ? t("unverify") : t("verify")}
</DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem <DropdownMenuItem
className="text-destructive" className="text-destructive"