mirror of
https://github.com/dyzulk/trustlab.git
synced 2026-01-27 07:05:44 +07:00
First commit
This commit is contained in:
201
src/components/user-profile/UserAddressCard.tsx
Normal file
201
src/components/user-profile/UserAddressCard.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
"use client";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
import axios from "@/lib/axios";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import React from "react";
|
||||
import { useModal } from "../../hooks/useModal";
|
||||
import { Modal } from "../ui/modal";
|
||||
import Button from "../ui/button/Button";
|
||||
import Input from "../form/input/InputField";
|
||||
import Label from "../form/Label";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
|
||||
|
||||
export default function UserAddressCard() {
|
||||
const { data: user, isLoading: userLoading } = useSWR("/api/user", fetcher);
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const { addToast } = useToast();
|
||||
const t = useTranslations("Profile");
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
|
||||
// Form states
|
||||
const [formData, setFormData] = React.useState({
|
||||
country: "",
|
||||
city_state: "",
|
||||
postal_code: "",
|
||||
tax_id: "",
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
setFormData({
|
||||
country: user.country || "",
|
||||
city_state: user.city_state || "",
|
||||
postal_code: user.postal_code || "",
|
||||
tax_id: user.tax_id || "",
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await axios.patch("/api/profile", formData);
|
||||
mutate("/api/user");
|
||||
addToast(t("toast_address_success"), "success");
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
addToast(t("toast_address_error"), "error");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (userLoading) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
{t("address_title")}
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
|
||||
<div>
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("country_label")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.country || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("city_state_label")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.city_state || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("postal_code_label")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.postal_code || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("tax_id_label")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.tax_id || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
{t("edit_button")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||
<div className="relative w-full p-4 overflow-y-auto bg-white no-scrollbar rounded-3xl dark:bg-gray-900 lg:p-11">
|
||||
<div className="px-2 pr-14">
|
||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
|
||||
{t("edit_address_title")}
|
||||
</h4>
|
||||
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
|
||||
{t("edit_address_subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<form className="flex flex-col" onSubmit={handleSave}>
|
||||
<div className="px-2 overflow-y-auto custom-scrollbar">
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div>
|
||||
<Label>{t("country_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.country}
|
||||
onChange={(e) => handleInputChange("country", e.target.value)}
|
||||
placeholder={t("country_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("city_state_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.city_state}
|
||||
onChange={(e) => handleInputChange("city_state", e.target.value)}
|
||||
placeholder={t("city_state_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("postal_code_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.postal_code}
|
||||
onChange={(e) => handleInputChange("postal_code", e.target.value)}
|
||||
placeholder={t("postal_code_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("tax_id_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.tax_id}
|
||||
onChange={(e) => handleInputChange("tax_id", e.target.value)}
|
||||
placeholder={t("tax_id_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
|
||||
<Button size="sm" variant="outline" type="button" onClick={closeModal} disabled={isSaving}>
|
||||
{t("close_button")}
|
||||
</Button>
|
||||
<Button size="sm" type="submit" loading={isSaving}>
|
||||
{t("save_changes_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
285
src/components/user-profile/UserInfoCard.tsx
Normal file
285
src/components/user-profile/UserInfoCard.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
"use client";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
import axios from "@/lib/axios";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import React from "react";
|
||||
import { useModal } from "../../hooks/useModal";
|
||||
import { Modal } from "../ui/modal";
|
||||
import Button from "../ui/button/Button";
|
||||
import Input from "../form/input/InputField";
|
||||
import Label from "../form/Label";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
|
||||
|
||||
export default function UserInfoCard() {
|
||||
const { data: user, isLoading: userLoading } = useSWR("/api/user", fetcher);
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const { addToast } = useToast();
|
||||
const t = useTranslations("Profile");
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
|
||||
// Form states
|
||||
const [formData, setFormData] = React.useState({
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
bio: "",
|
||||
facebook: "",
|
||||
twitter: "",
|
||||
linkedin: "",
|
||||
instagram: "",
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
setFormData({
|
||||
first_name: user.first_name || "",
|
||||
last_name: user.last_name || "",
|
||||
email: user.email || "",
|
||||
phone: user.phone || "",
|
||||
bio: user.bio || "",
|
||||
facebook: user.facebook || "",
|
||||
twitter: user.twitter || "",
|
||||
linkedin: user.linkedin || "",
|
||||
instagram: user.instagram || "",
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await axios.patch("/api/profile", formData);
|
||||
mutate("/api/user");
|
||||
addToast(t("toast_personal_info_success"), "success");
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
addToast(t("toast_personal_info_error"), "error");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (userLoading) return null;
|
||||
|
||||
return (
|
||||
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
||||
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
|
||||
<div>
|
||||
<h4 className="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
{t("personal_info_title")}
|
||||
</h4>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
|
||||
<div>
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("first_name_label")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.first_name || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("last_name_label")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.last_name || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("email_label")}
|
||||
</p>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.email}
|
||||
</p>
|
||||
{user?.pending_email && (
|
||||
<span className="text-[10px] bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400 px-2 py-0.5 rounded-full inline-block w-fit font-medium">
|
||||
{t("pending_email_notice", { email: user.pending_email })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("phone_label")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.phone || "-"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
|
||||
{t("bio_label")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
|
||||
{user?.bio || "-"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
{t("edit_button")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||
<div className="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
|
||||
<div className="px-2 pr-14">
|
||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
|
||||
{t("edit_personal_info_title")}
|
||||
</h4>
|
||||
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
|
||||
{t("edit_personal_info_subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<form className="flex flex-col" onSubmit={handleSave}>
|
||||
<div className="custom-scrollbar h-[450px] overflow-y-auto px-2 pb-3">
|
||||
<div>
|
||||
<h5 className="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
{t("social_links_title")}
|
||||
</h5>
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div>
|
||||
<Label>{t("facebook_username_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.facebook}
|
||||
onChange={(e) => handleInputChange("facebook", e.target.value)}
|
||||
placeholder={t("username_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("twitter_username_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.twitter}
|
||||
onChange={(e) => handleInputChange("twitter", e.target.value)}
|
||||
placeholder={t("username_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("linkedin_username_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.linkedin}
|
||||
onChange={(e) => handleInputChange("linkedin", e.target.value)}
|
||||
placeholder="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("instagram_username_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.instagram}
|
||||
onChange={(e) => handleInputChange("instagram", e.target.value)}
|
||||
placeholder={t("username_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-7">
|
||||
<h5 className="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
|
||||
{t("personal_info_title")}
|
||||
</h5>
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div className="col-span-2 lg:col-span-1">
|
||||
<Label>{t("first_name_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.first_name}
|
||||
onChange={(e) => handleInputChange("first_name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 lg:col-span-1">
|
||||
<Label>{t("last_name_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.last_name}
|
||||
onChange={(e) => handleInputChange("last_name", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 lg:col-span-1">
|
||||
<Label>{t("email_label")}</Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||
placeholder={t("email_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 lg:col-span-1">
|
||||
<Label>{t("phone_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label>{t("bio_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={formData.bio}
|
||||
onChange={(e) => handleInputChange("bio", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
|
||||
<Button size="sm" variant="outline" type="button" onClick={closeModal} disabled={isSaving}>
|
||||
{t("close_button")}
|
||||
</Button>
|
||||
<Button size="sm" type="submit" loading={isSaving}>
|
||||
{t("save_changes_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
300
src/components/user-profile/UserMetaCard.tsx
Normal file
300
src/components/user-profile/UserMetaCard.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
"use client";
|
||||
|
||||
import useSWR, { mutate } from "swr";
|
||||
import axios from "@/lib/axios";
|
||||
import ImageCropper from "../common/ImageCropper";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import React from "react";
|
||||
import { useModal } from "../../hooks/useModal";
|
||||
import { Modal } from "../ui/modal";
|
||||
import Button from "../ui/button/Button";
|
||||
import Input from "../form/input/InputField";
|
||||
import Label from "../form/Label";
|
||||
import Image from "next/image";
|
||||
import { getUserAvatar } from "@/lib/utils";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const fetcher = (url: string) => axios.get(url).then((res) => res.data);
|
||||
|
||||
export default function UserMetaCard() {
|
||||
const { data: user, isLoading: userLoading } = useSWR("/api/user", fetcher);
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const { addToast } = useToast();
|
||||
const t = useTranslations("Profile");
|
||||
|
||||
const [selectedFile, setSelectedFile] = React.useState<string | null>(null);
|
||||
const [isCropperOpen, setIsCropperOpen] = React.useState(false);
|
||||
const [isSaving, setIsSaving] = React.useState(false);
|
||||
|
||||
// Form states for Meta Info
|
||||
const [jobTitle, setJobTitle] = React.useState("");
|
||||
const [location, setLocation] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
if (user) {
|
||||
setJobTitle(user.job_title || "");
|
||||
setLocation(user.location || "");
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.files && e.target.files.length > 0) {
|
||||
const file = e.target.files[0];
|
||||
|
||||
// Check for file size limit (5MB)
|
||||
const maxSize = 5 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
addToast(t("toast_file_size_error"), "error");
|
||||
// Reset input
|
||||
e.target.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("load", () => {
|
||||
setSelectedFile(reader.result as string);
|
||||
setIsCropperOpen(true);
|
||||
});
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCropComplete = async (croppedImage: Blob) => {
|
||||
const formData = new FormData();
|
||||
formData.append("avatar", croppedImage, "avatar.png");
|
||||
|
||||
try {
|
||||
addToast(t("toast_updating_avatar"), "info");
|
||||
await axios.post("/api/profile/avatar", formData, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
mutate("/api/user");
|
||||
mutate("/api/dashboard");
|
||||
addToast(t("toast_avatar_success"), "success");
|
||||
} catch (err) {
|
||||
addToast(t("toast_avatar_error"), "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await axios.patch("/api/profile", {
|
||||
job_title: jobTitle,
|
||||
location: location,
|
||||
});
|
||||
mutate("/api/user");
|
||||
addToast(t("toast_profile_success"), "success");
|
||||
closeModal();
|
||||
} catch (err) {
|
||||
addToast(t("toast_profile_error"), "error");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (userLoading) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
||||
<div className="flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between">
|
||||
<div className="flex flex-col items-center w-full gap-6 xl:flex-row">
|
||||
<div className="group relative w-20 h-20 overflow-hidden border border-gray-200 rounded-full dark:border-gray-800">
|
||||
<Image
|
||||
width={80}
|
||||
height={80}
|
||||
src={getUserAvatar(user)}
|
||||
alt="user"
|
||||
className="object-cover w-full h-full"
|
||||
unoptimized={true}
|
||||
/>
|
||||
<label className="absolute inset-0 flex items-center justify-center bg-black/40 opacity-0 transition-opacity group-hover:opacity-100 cursor-pointer">
|
||||
<span className="text-white text-[10px] font-medium uppercase">{t("change_avatar")}</span>
|
||||
<input type="file" className="hidden" accept="image/*" onChange={handleFileChange} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="order-3 xl:order-2">
|
||||
<h4 className="mb-2 text-lg font-semibold text-center text-gray-800 dark:text-white/90 xl:text-left">
|
||||
{user?.first_name} {user?.last_name}
|
||||
</h4>
|
||||
<div className="flex flex-col items-center gap-1 text-center xl:flex-row xl:gap-3 xl:text-left">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{user?.job_title || t("no_job_title")}
|
||||
</p>
|
||||
<div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{user?.location || t("no_location")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center order-2 gap-2 grow xl:order-3 xl:justify-end">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href={user?.facebook ? `https://facebook.com/${user.facebook}` : '#'}
|
||||
className={`flex h-11 w-11 items-center justify-center gap-2 rounded-full border border-gray-300 bg-white text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 ${!user?.facebook && 'opacity-50 pointer-events-none'}`}
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11.6666 11.2503H13.7499L14.5833 7.91699H11.6666V6.25033C11.6666 5.39251 11.6666 4.58366 13.3333 4.58366H14.5833V1.78374C14.3118 1.7477 13.2858 1.66699 12.2023 1.66699C9.94025 1.66699 8.33325 3.04771 8.33325 5.58342V7.91699H5.83325V11.2503H8.33325V18.3337H11.6666V11.2503Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={user?.twitter ? `https://x.com/${user.twitter}` : '#'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={`flex h-11 w-11 items-center justify-center gap-2 rounded-full border border-gray-300 bg-white text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 ${!user?.twitter && 'opacity-50 pointer-events-none'}`}
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.1708 1.875H17.9274L11.9049 8.75833L18.9899 18.125H13.4424L9.09742 12.4442L4.12578 18.125H1.36745L7.80912 10.7625L1.01245 1.875H6.70078L10.6283 7.0675L15.1708 1.875ZM14.2033 16.475H15.7308L5.87078 3.43833H4.23162L14.2033 16.475Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={user?.linkedin ? `https://linkedin.com/in/${user.linkedin}` : "#"}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={`flex h-11 w-11 items-center justify-center gap-2 rounded-full border border-gray-300 bg-white text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 ${!user?.linkedin && 'opacity-50 pointer-events-none'}`}
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M5.78381 4.16645C5.78351 4.84504 5.37181 5.45569 4.74286 5.71045C4.11391 5.96521 3.39331 5.81321 2.92083 5.32613C2.44836 4.83904 2.31837 4.11413 2.59216 3.49323C2.86596 2.87233 3.48886 2.47942 4.16715 2.49978C5.06804 2.52682 5.78422 3.26515 5.78381 4.16645ZM5.83381 7.06645H2.50048V17.4998H5.83381V7.06645ZM11.1005 7.06645H7.78381V17.4998H11.0672V12.0248C11.0672 8.97475 15.0422 8.69142 15.0422 12.0248V17.4998H18.3338V10.8914C18.3338 5.74978 12.4505 5.94145 11.0672 8.46642L11.1005 7.06645Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={user?.instagram ? `https://instagram.com/${user.instagram}` : '#'}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={`flex h-11 w-11 items-center justify-center gap-2 rounded-full border border-gray-300 bg-white text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 ${!user?.instagram && 'opacity-50 pointer-events-none'}`}
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.8567 1.66699C11.7946 1.66854 12.2698 1.67351 12.6805 1.68573L12.8422 1.69102C13.0291 1.69766 13.2134 1.70599 13.4357 1.71641C14.3224 1.75738 14.9273 1.89766 15.4586 2.10391C16.0078 2.31572 16.4717 2.60183 16.9349 3.06503C17.3974 3.52822 17.6836 3.99349 17.8961 4.54141C18.1016 5.07197 18.2419 5.67753 18.2836 6.56433C18.2935 6.78655 18.3015 6.97088 18.3081 7.15775L18.3133 7.31949C18.3255 7.73011 18.3311 8.20543 18.3328 9.1433L18.3335 9.76463C18.3336 9.84055 18.3336 9.91888 18.3336 9.99972L18.3335 10.2348L18.333 10.8562C18.3314 11.794 18.3265 12.2694 18.3142 12.68L18.3089 12.8417C18.3023 13.0286 18.294 13.213 18.2836 13.4351C18.2426 14.322 18.1016 14.9268 17.8961 15.458C17.6842 16.0074 17.3974 16.4713 16.9349 16.9345C16.4717 17.397 16.0057 17.6831 15.4586 17.8955C14.9273 18.1011 14.3224 18.2414 13.4357 18.2831C13.2134 18.293 13.0291 18.3011 12.8422 18.3076L12.6805 18.3128C12.2698 18.3251 11.7946 18.3306 10.8567 18.3324L10.2353 18.333C10.1594 18.333 10.0811 18.333 10.0002 18.333H9.76516L9.14375 18.3325C8.20591 18.331 7.7306 18.326 7.31997 18.3137L7.15824 18.3085C6.97136 18.3018 6.78703 18.2935 6.56481 18.2831C5.67801 18.2421 5.07384 18.1011 4.5419 17.8955C3.99328 17.6838 3.5287 17.397 3.06551 16.9345C2.60231 16.4713 2.3169 16.0053 2.1044 15.458C1.89815 14.9268 1.75856 14.322 1.7169 13.4351C1.707 13.213 1.69892 13.0286 1.69238 12.8417L1.68714 12.68C1.67495 12.2694 1.66939 11.794 1.66759 10.8562L1.66748 9.1433C1.66903 8.20543 1.67399 7.73011 1.68621 7.31949L1.69151 7.15775C1.69815 6.97088 1.70648 6.78655 1.7169 6.56433C1.75786 5.67683 1.89815 5.07266 2.1044 4.54141C2.3162 3.9928 2.60231 3.52822 3.06551 3.06503C3.5287 2.60183 3.99398 2.31641 4.5419 2.10391C5.07315 1.89766 5.67731 1.75808 6.56481 1.71641C6.78703 1.70652 6.97136 1.69844 7.15824 1.6919L7.31997 1.68666C7.7306 1.67446 8.20591 1.6689 9.14375 1.6671L10.8567 1.66699ZM10.0002 5.83308C7.69781 5.83308 5.83356 7.69935 5.83356 9.99972C5.83356 12.3021 7.69984 14.1664 10.0002 14.1664C12.3027 14.1664 14.1669 12.3001 14.1669 9.99972C14.1669 7.69732 12.3006 5.83308 10.0002 5.83308ZM10.0002 7.49974C11.381 7.49974 12.5002 8.61863 12.5002 9.99972C12.5002 11.3805 11.3813 12.4997 10.0002 12.4997C8.6195 12.4997 7.50023 11.3809 7.50023 9.99972C7.50023 8.61897 8.61908 7.49974 10.0002 7.49974ZM14.3752 4.58308C13.8008 4.58308 13.3336 5.04967 13.3336 5.62403C13.3336 6.19841 13.8002 6.66572 14.3752 6.66572C14.9496 6.66572 15.4169 6.19913 15.4169 5.62403C15.4169 5.04967 14.9488 4.58236 14.3752 4.58308Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={openModal}
|
||||
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
|
||||
>
|
||||
<svg
|
||||
className="fill-current"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
|
||||
fill=""
|
||||
/>
|
||||
</svg>
|
||||
{t("edit_button")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
|
||||
<div className="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
|
||||
<div className="px-2 pr-14">
|
||||
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
|
||||
{t("edit_metadata_title")}
|
||||
</h4>
|
||||
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
|
||||
{t("edit_metadata_subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<form className="flex flex-col" onSubmit={handleSave}>
|
||||
<div className="custom-scrollbar px-2 pb-3">
|
||||
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
|
||||
<div className="col-span-2 lg:col-span-1">
|
||||
<Label>{t("job_title_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={jobTitle}
|
||||
onChange={(e) => setJobTitle(e.target.value)}
|
||||
placeholder={t("job_title_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 lg:col-span-1">
|
||||
<Label>{t("location_label")}</Label>
|
||||
<Input
|
||||
type="text"
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
placeholder={t("location_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
|
||||
<Button size="sm" variant="outline" type="button" onClick={closeModal} disabled={isSaving}>
|
||||
{t("close_button")}
|
||||
</Button>
|
||||
<Button size="sm" type="submit" loading={isSaving}>
|
||||
{t("save_changes_button")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{selectedFile && (
|
||||
<ImageCropper
|
||||
image={selectedFile}
|
||||
isOpen={isCropperOpen}
|
||||
onClose={() => {
|
||||
setIsCropperOpen(false);
|
||||
setSelectedFile(null);
|
||||
}}
|
||||
onCropComplete={handleCropComplete}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
99
src/components/user-profile/UserPasswordCard.tsx
Normal file
99
src/components/user-profile/UserPasswordCard.tsx
Normal file
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import axios from "@/lib/axios";
|
||||
import Button from "../ui/button/Button";
|
||||
import Input from "../form/input/InputField";
|
||||
import Label from "../form/Label";
|
||||
import { useToast } from "@/context/ToastContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function UserPasswordCard() {
|
||||
const { addToast } = useToast();
|
||||
const t = useTranslations("Profile");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
current_password: "",
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
});
|
||||
|
||||
const handleInputChange = (field: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (formData.password !== formData.password_confirmation) {
|
||||
addToast(t("toast_password_mismatch"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
await axios.put("/api/profile/password", formData);
|
||||
addToast(t("toast_password_success"), "success");
|
||||
setFormData({
|
||||
current_password: "",
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
});
|
||||
} catch (err: any) {
|
||||
const message = err.response?.data?.message || t("toast_password_error");
|
||||
addToast(message, "error");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
|
||||
<h4 className="mb-6 text-lg font-semibold text-gray-800 dark:text-white/90">
|
||||
{t("update_password_title")}
|
||||
</h4>
|
||||
|
||||
<form onSubmit={handleSave} className="space-y-5">
|
||||
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2 lg:gap-6">
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<Label>{t("current_password_label")}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={formData.current_password}
|
||||
onChange={(e) => handleInputChange("current_password", e.target.value)}
|
||||
placeholder={t("current_password_placeholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("new_password_label")}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
||||
placeholder={t("new_password_placeholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>{t("confirm_password_label")}</Label>
|
||||
<Input
|
||||
type="password"
|
||||
value={formData.password_confirmation}
|
||||
onChange={(e) => handleInputChange("password_confirmation", e.target.value)}
|
||||
placeholder={t("confirm_password_placeholder")}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" loading={isSaving}>
|
||||
{t("update_password_title")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user