Add name/email editing and days-based course access in admin user card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 12:01:01 +05:00
parent e691124058
commit 6b5bfc853e
4 changed files with 69 additions and 15 deletions
+37 -6
View File
@@ -13,14 +13,23 @@ const inputStyle = {
fontFamily: "inherit",
} as React.CSSProperties;
const focusHandlers = {
onFocus: (e: React.FocusEvent<HTMLInputElement>) => (e.currentTarget.style.borderColor = "var(--foreground)"),
onBlur: (e: React.FocusEvent<HTMLInputElement>) => (e.currentTarget.style.borderColor = "var(--border)"),
};
interface Props {
userId: string;
name: string;
email: string;
phone: string | null;
birthday: Date | null;
}
export function UserContactEditor({ userId, phone, birthday }: Props) {
export function UserContactEditor({ userId, name, email, phone, birthday }: Props) {
const [editing, setEditing] = useState(false);
const [nameVal, setNameVal] = useState(name);
const [emailVal, setEmailVal] = useState(email);
const [phoneVal, setPhoneVal] = useState(phone ?? "");
const [birthdayVal, setBirthdayVal] = useState(
birthday ? birthday.toISOString().slice(0, 10) : ""
@@ -29,7 +38,7 @@ export function UserContactEditor({ userId, phone, birthday }: Props) {
function handleSave() {
startTransition(async () => {
await updateUserContact(userId, { phone: phoneVal, birthday: birthdayVal });
await updateUserContact(userId, { name: nameVal, email: emailVal, phone: phoneVal, birthday: birthdayVal });
setEditing(false);
});
}
@@ -68,6 +77,30 @@ export function UserContactEditor({ userId, phone, birthday }: Props) {
return (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-1">
<label className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Имя
</label>
<input
type="text"
value={nameVal}
onChange={(e) => setNameVal(e.target.value)}
style={inputStyle}
{...focusHandlers}
/>
</div>
<div className="space-y-1">
<label className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Email
</label>
<input
type="email"
value={emailVal}
onChange={(e) => setEmailVal(e.target.value)}
style={inputStyle}
{...focusHandlers}
/>
</div>
<div className="space-y-1">
<label className="text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
Телефон
@@ -78,8 +111,7 @@ export function UserContactEditor({ userId, phone, birthday }: Props) {
onChange={(e) => setPhoneVal(e.target.value)}
placeholder="+7 900 000-00-00"
style={inputStyle}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
{...focusHandlers}
/>
</div>
<div className="space-y-1">
@@ -91,8 +123,7 @@ export function UserContactEditor({ userId, phone, birthday }: Props) {
value={birthdayVal}
onChange={(e) => setBirthdayVal(e.target.value)}
style={inputStyle}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
{...focusHandlers}
/>
</div>
</div>
@@ -1,8 +1,6 @@
"use client";
import { useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { bulkGrantAccess, revokeUserAccess } from "@/lib/actions/user-actions";
interface Course {
@@ -28,7 +26,7 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props
() => new Map(enrollments.map((e) => [e.courseId, e.expiresAt]))
);
const [selected, setSelected] = useState<Set<string>>(new Set());
const [expiryDate, setExpiryDate] = useState("");
const [days, setDays] = useState("");
const [pending, startTransition] = useTransition();
const unenrolled = allCourses.filter((c) => !enrolledMap.has(c.id));
@@ -44,12 +42,16 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props
function handleBulkGrant() {
if (selected.size === 0) return;
const ids = [...selected];
const expiry = expiryDate || null;
const daysNum = parseInt(days, 10);
const expiresAt = !isNaN(daysNum) && daysNum > 0
? new Date(Date.now() + daysNum * 86_400_000).toISOString()
: null;
const newMap = new Map(enrolledMap);
ids.forEach((id) => newMap.set(id, expiry ? new Date(expiry) : null));
ids.forEach((id) => newMap.set(id, expiresAt ? new Date(expiresAt) : null));
setEnrolledMap(newMap);
setSelected(new Set());
startTransition(() => bulkGrantAccess(userId, ids, expiry));
setDays("");
startTransition(() => bulkGrantAccess(userId, ids, expiresAt));
}
function handleRevoke(courseId: string) {
@@ -119,9 +121,26 @@ export function UserEnrollmentManager({ userId, allCourses, enrollments }: Props
<div className="flex items-center gap-3">
<div>
<label className="text-xs uppercase tracking-wider block mb-1" style={{ color: "var(--muted-foreground)" }}>
Срок доступа
Дней (0 бессрочно)
</label>
<Input type="date" value={expiryDate} onChange={(e) => setExpiryDate(e.target.value)} className="w-44" />
<input
type="number"
min="0"
value={days}
onChange={(e) => setDays(e.target.value)}
placeholder="0"
style={{
border: "2px solid var(--border)",
background: "var(--background)",
outline: "none",
padding: "0.4rem 0.6rem",
fontSize: "0.875rem",
fontFamily: "inherit",
width: "6rem",
}}
onFocus={(e) => (e.currentTarget.style.borderColor = "var(--foreground)")}
onBlur={(e) => (e.currentTarget.style.borderColor = "var(--border)")}
/>
</div>
<div className="pt-5">
<button onClick={handleBulkGrant} disabled={pending} className="btn-aubade btn-aubade-accent">