Skip to content
Merged
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
5 changes: 5 additions & 0 deletions apps/sale/app/tutor/profile/edit/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ProfileEditorSkeleton } from "@/features/tutor-profile-editor";

export default function Loading() {
return <ProfileEditorSkeleton />;
}
2 changes: 1 addition & 1 deletion apps/sale/features/messages/components/ChatSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export function ChatSidebar() {
className="text-sm tracking-tight text-[#280f91]"
style={{ fontFamily: "var(--font-nunito-family)", fontWeight: 800 }}
>
CỘNG ĐỒNG GIA SƯ
CỘNG ĐỒNG GIA SƯ BEEWISE
</span>
</div>

Expand Down
7 changes: 5 additions & 2 deletions apps/sale/features/messages/components/MessagesScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ export function MessagesScreen() {
</div>
<span
className="text-xl tracking-tight text-[#280f91]"
style={{ fontFamily: "var(--font-nunito-family)", fontWeight: 800 }}
style={{
fontFamily: "var(--font-nunito-family)",
fontWeight: 800,
}}
>
CỘNG ĐỒNG GIA SƯ
CỘNG ĐỒNG GIA SƯ BEEWISE
</span>
</div>
<h2 className="mb-2 text-lg font-extrabold text-[#0c0c0b]">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useRouter } from "next/navigation";
import { TutorApprovedProvider } from "./TutorApprovedProvider";
import { TutorApprovedScreenView, TutorApprovedShell } from "./tutor-approved.ui";
import { resolveTutorApprovedScenario, parseTutorApprovedScenario } from "../schemas/tutor-approved.resolver";
import { parseTutorApprovedScenario } from "../schemas/tutor-approved.resolver";
import { tutorApprovedScenarios, type TutorApprovedScenario, type TutorApprovedScreen } from "../types";

const previewUser = {
Expand Down Expand Up @@ -31,6 +31,7 @@ function scenarioScreen(scenario: TutorApprovedScenario): TutorApprovedScreen {

export function TutorApprovedPreview({ scenario, capture }: { scenario?: string | null; capture: boolean }) {
const parsed = parseTutorApprovedScenario(scenario);
const screen = scenarioScreen(parsed);
const router = useRouter();
const navigate = (next: TutorApprovedScenario, nextCapture = capture) => {
const params = new URLSearchParams({ scenario: next });
Expand All @@ -40,11 +41,12 @@ export function TutorApprovedPreview({ scenario, capture }: { scenario?: string
return (
<TutorApprovedProvider scenario={parsed}>
<TutorApprovedShell
screen={screen}
previewUser={previewUser}
capture={capture}
toolbar={!capture ? <div className="fixed left-1/2 top-18 z-90 flex -translate-x-1/2 flex-wrap items-center justify-center gap-2 rounded-full border bg-white/95 px-3 py-2 shadow-lg backdrop-blur"><span className="text-xs font-bold text-[#905b0f]">PREVIEW</span><label className="sr-only" htmlFor="approved-scenario">Scenario</label><select id="approved-scenario" value={parsed} onChange={(e)=>navigate(e.target.value as TutorApprovedScenario,false)} className="rounded-full border px-3 py-1 text-xs">{tutorApprovedScenarios.map((item: string)=><option key={item}>{item}</option>)}</select><button onClick={()=>navigate(parsed,true)} className="text-xs font-bold text-[#280f91]">Capture mode</button></div> : null}
>
<TutorApprovedScreenView screen={scenarioScreen(parsed)} />
<TutorApprovedScreenView screen={screen} />
</TutorApprovedShell>
</TutorApprovedProvider>
);
Expand Down
139 changes: 13 additions & 126 deletions apps/sale/features/tutor-approved/components/tutor-approved.ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { TUTOR_LMS_URL } from "@workspace/core/constants/tutor-links";
import { useTutorApproved } from "./TutorApprovedProvider";
import type { TutorApprovedScreen } from "../types";
import { ChatSummary, MessagesScreen, ChatRoomScreen } from "../../messages";
import { TutorProfileEditorScreen } from "../../tutor-profile-editor";
import { TutorOwnProfileScreen } from "../../tutor-profile/components/TutorOwnProfileScreen";

const card =
"rounded-2xl border border-[#dce7f7] bg-white p-5 shadow-[0_12px_32px_rgba(40,15,145,0.07)]";
Expand All @@ -46,6 +48,15 @@ export function TutorApprovedShell({
screen?: TutorApprovedScreen;
}) {
const isChat = screen === "messages" || screen === "chat-room";
const isProfileEdit = screen === "profile-edit";

if (isProfileEdit) {
return (
<div className="min-h-[100dvh] bg-[#f7f9fd] text-[#17142f]">
{children}
</div>
);
}

if (isChat) {
return (
Expand Down Expand Up @@ -84,8 +95,8 @@ export function TutorApprovedScreenView({
screen?: TutorApprovedScreen;
chatRoomId?: string;
}) {
if (screen === "profile" || screen === "profile-edit")
return <ProfileScreen edit={screen === "profile-edit"} />;
if (screen === "profile-edit") return <TutorProfileEditorScreen />;
if (screen === "profile") return <TutorOwnProfileScreen />;
if (screen === "messages") return <MessagesScreen />;
if (screen === "chat-room") return <ChatRoomScreen chatRoomId={chatRoomId} />;
if (screen === "notifications") return <NotificationsScreen />;
Expand Down Expand Up @@ -476,130 +487,6 @@ function RecentActivity() {
);
}

function ProfileScreen({ edit }: { edit: boolean }) {
const { state, dispatch } = useTutorApproved();
const p = edit ? state.draftProfile : state.profile;
const fields:
| [[keyof typeof p, string, boolean]]
| Array<[keyof typeof p, string, boolean]> = [
["title", "Tiêu đề hồ sơ", true],
["introduction", "Giới thiệu ngắn", true],
["description", "Mô tả", true],
["teachingMethod", "Phương pháp giảng dạy", false],
["experience", "Kinh nghiệm", false],
["rate", "Học phí dự kiến", true],
];
return (
<div className="mx-auto max-w-4xl space-y-6">
<div>
<p className="text-sm font-bold text-[#280f91]">Hồ sơ gia sư</p>
<h1 className="font-nunito text-3xl font-black">
{edit ? "Chỉnh sửa hồ sơ" : "Hồ sơ của tôi"}
</h1>
<p className="mt-2 text-sm text-[#716c83]">
Thông tin ngân hàng và giấy tờ xác thực không xuất hiện trong hồ sơ
công khai.
</p>
</div>
{state.profileSubmitted && (
<div
role="status"
className="rounded-xl border border-[#ffc510] bg-[#fff8df] p-4 text-sm"
>
Thay đổi đã được gửi. Hồ sơ công khai hiện tại vẫn được giữ cho đến
khi Consultant phê duyệt nội dung mới.
</div>
)}
<section className={card}>
<div className="mb-5 flex items-center gap-4">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-[#cfe1fa] text-xl font-black text-[#280f91]">
MA
</div>
<div>
<h2 className="text-xl font-black">{p.fullName}</h2>
<span className={`${badge} bg-[#e7f5ec] text-[#297044]`}>
<ShieldCheck size={13} /> Đã xác thực
</span>
</div>
</div>
{edit ? (
<form
onSubmit={(e) => {
e.preventDefault();
dispatch({ type: "submit-profile" });
}}
className="space-y-4"
>
{fields.map(([field, label, review]) => (
<label key={field} className="block text-sm font-bold">
{label}
{review && (
<span className="ml-2 text-xs font-normal text-[#905b0f]">
Cần xét duyệt lại
</span>
)}
<textarea
value={String(p[field])}
onChange={(e) =>
dispatch({
type: "update-draft",
field,
value: e.target.value,
})
}
className="mt-1 min-h-20 w-full rounded-xl border border-[#dce7f7] p-3 font-normal outline-none focus:ring-2 focus:ring-[#280f91]"
/>
</label>
))}
<div className="flex flex-wrap gap-3">
<Button type="button" variant="outline">
Lưu bản nháp
</Button>
<Button type="button" variant="outline">
Xem trước
</Button>
<Button type="submit">Gửi thay đổi</Button>
<Button
type="button"
variant="ghost"
onClick={() => dispatch({ type: "reset-draft" })}
>
Hủy chỉnh sửa
</Button>
</div>
</form>
) : (
<div className="space-y-5">
<Info label="Giới thiệu" value={p.description} />
<Info
label="Trường / Chuyên ngành"
value={`${p.university} · ${p.major}`}
/>
<Info
label="Môn và cấp lớp"
value={`${p.subjects.join(", ")} · ${p.gradeLevels.join(", ")}`}
/>
<Info
label="Hình thức / Khu vực"
value={`${p.teachingMode} · ${p.offlineAreas.join(", ")}`}
/>
<Info label="Phương pháp" value={p.teachingMethod} />
<Info label="Kinh nghiệm" value={p.experience} />
<Button asChild>
<Link href="/tutor/profile/edit">
<Pencil />
Chỉnh sửa hồ sơ
</Link>
</Button>
</div>
)}
</section>
</div>
);
}



function NotificationsScreen() {
const { state, dispatch } = useTutorApproved();
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"use client";

import { useFieldArray, useFormContext } from "react-hook-form";
import type { TutorProfileEditorValues } from "../schemas/profile-editor.schema";
import { ProfileField } from "./ProfileField";
import { RepeaterError, RepeaterHeader, RepeaterItem } from "./RepeaterPrimitives";

const DAY_OPTIONS = [
{ value: "Thứ 2", label: "Thứ 2" },
{ value: "Thứ 3", label: "Thứ 3" },
{ value: "Thứ 4", label: "Thứ 4" },
{ value: "Thứ 5", label: "Thứ 5" },
{ value: "Thứ 6", label: "Thứ 6" },
{ value: "Thứ 7", label: "Thứ 7" },
{ value: "Chủ nhật", label: "Chủ nhật" },
];

export function AvailabilityListField() {
const {
control,
register,
formState: { errors },
} = useFormContext<TutorProfileEditorValues>();
const { fields, append, remove } = useFieldArray({
control,
name: "availability",
});

return (
<div className="grid gap-3">
<RepeaterHeader
label="Lịch rảnh dự kiến"
description="Tách riêng ngày dạy và khung giờ để học viên dễ theo dõi."
addLabel="Thêm lịch rảnh"
onAdd={() => append({ day: "", time: "" })}
canAdd={fields.length < 10}
/>
<div className="grid gap-3">
{fields.map((field, index) => (
<RepeaterItem
key={field.id}
title={`Lịch rảnh ${index + 1}`}
canRemove={fields.length > 1}
onRemove={() => remove(index)}
>
<div className="grid gap-4 md:grid-cols-2">
<ProfileField
id={`availability-${field.id}-day`}
label="Ngày dạy"
error={errors.availability?.[index]?.day}
registration={register(`availability.${index}.day`)}
options={DAY_OPTIONS}
/>
<ProfileField
id={`availability-${field.id}-time`}
label="Khung giờ"
error={errors.availability?.[index]?.time}
registration={register(`availability.${index}.time`)}
placeholder="Ví dụ: 19:00 - 21:00"
/>
</div>
</RepeaterItem>
))}
</div>
<RepeaterError message={errors.availability?.root?.message} />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ReactNode } from "react";
import { LockSimpleIcon, PencilSimpleIcon } from "@phosphor-icons/react";

interface EditableProfileBlockProps {
label: string;
children: ReactNode;
onEdit?: () => void;
systemManaged?: boolean;
}

export function EditableProfileBlock({
label,
children,
onEdit,
systemManaged = false,
}: EditableProfileBlockProps) {
return (
<div className="group relative">
<div className="absolute right-3 top-3 z-10 sm:right-4 sm:top-4">
{systemManaged ? (
<span className="inline-flex h-9 items-center gap-1.5 rounded-full border border-[#cfe1fa] bg-white px-3 text-xs font-bold text-[#56516a] shadow-sm">
<LockSimpleIcon size={14} weight="bold" aria-hidden="true" />
Dữ liệu hệ thống
</span>
) : (
<button
type="button"
onClick={onEdit}
aria-label={`Chỉnh sửa ${label.toLowerCase()}`}
className="inline-flex h-10 w-10 items-center justify-center rounded-full border border-[#280f91]/20 bg-white text-[#280f91] shadow-md transition hover:-translate-y-0.5 hover:bg-[#280f91] hover:text-white active:translate-y-0 active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#280f91]/30"
>
<PencilSimpleIcon size={18} weight="bold" aria-hidden="true" />
</button>
)}
</div>
{children}
</div>
);
}
Loading
Loading