Compare commits

..
226 changed files with 13017 additions and 50635 deletions

No files matched your search

-18
View File
@@ -1,18 +0,0 @@
{
"permissions": {
"allow": [
"Bash(npx nx run-many:*)",
"Bash(npm install:*)",
"Bash(npm view:*)",
"Bash(npx nx build landing)",
"Bash(npm uninstall:*)",
"Bash(dir:*)",
"Bash(ren page.tsx page-original.tsx)",
"Bash(ren:*)",
"Bash(npx supabase:*)",
"Bash(libs/service/src/types/supabase.ts)"
],
"deny": [],
"ask": []
}
}
-55
View File
@@ -1,55 +0,0 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Build outputs
dist
.next
out
build
coverage
# IDE
.vscode
.idea
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Git
.git
.gitignore
# Environment
.env
.env.local
.env.*.local
# Testing
coverage
.nyc_output
# Logs
logs
*.log
# Nx
.nx
# Docker
Dockerfile
docker-compose*.yml
.dockerignore
# Misc
*.md
!README.md
.editorconfig
.prettierrc
.eslintrc
-3
View File
@@ -1,3 +0,0 @@
VITE_SUPABASE_URL=https://your-project-id.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key-here
+3 -12
View File
@@ -7,10 +7,6 @@ on:
paths:
- 'apps/backoffice/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
concurrency:
group: deploy-vps
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
@@ -25,20 +21,15 @@ jobs:
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
command_timeout: 30m
script: |
set -e
if [ ! -d ~/imphnen-frontend-service-backoffice ]; then
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-backoffice
else
cd ~/imphnen-frontend-service-backoffice && git fetch origin && git reset --hard origin/develop && git pull
cd ~/imphnen-frontend-service-backoffice && git pull
fi
cat > ~/imphnen-frontend-service-backoffice/apps/backoffice/.env << EOF
VITE_API_URL=${{ secrets.VITE_API_URL }}
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
EOF
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-backoffice/apps/backoffice/.env
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml down || true
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml build --progress=plain
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml build
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml up -d
docker image prune -af
+3 -12
View File
@@ -7,10 +7,6 @@ on:
paths:
- 'apps/dimentorin/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
concurrency:
group: deploy-vps
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
@@ -25,20 +21,15 @@ jobs:
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
command_timeout: 30m
script: |
set -e
if [ ! -d ~/imphnen-frontend-service-dimentorin ]; then
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-dimentorin
else
cd ~/imphnen-frontend-service-dimentorin && git fetch origin && git reset --hard origin/develop && git pull
cd ~/imphnen-frontend-service-dimentorin && git pull
fi
cat > ~/imphnen-frontend-service-dimentorin/apps/dimentorin/.env << EOF
VITE_API_URL=${{ secrets.VITE_API_URL }}
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
EOF
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-dimentorin/apps/dimentorin/.env
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml down || true
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml build --progress=plain
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml build
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml up -d
docker image prune -af
+3 -12
View File
@@ -7,10 +7,6 @@ on:
paths:
- 'apps/gacha/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
concurrency:
group: deploy-vps
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
@@ -25,20 +21,15 @@ jobs:
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
command_timeout: 30m
script: |
set -e
if [ ! -d ~/imphnen-frontend-service-gacha ]; then
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-gacha
else
cd ~/imphnen-frontend-service-gacha && git fetch origin && git reset --hard origin/develop && git pull
cd ~/imphnen-frontend-service-gacha && git pull
fi
cat > ~/imphnen-frontend-service-gacha/apps/gacha/.env << EOF
VITE_API_URL=${{ secrets.VITE_API_URL }}
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
EOF
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-gacha/apps/gacha/.env
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml down || true
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml build --progress=plain
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml build
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml up -d
docker image prune -af
-45
View File
@@ -1,45 +0,0 @@
name: Deploy Hackathon
on:
push:
branches:
- develop
paths:
- 'apps/hackathon/**' # Auto deploy when changes pushed to apps/hackathon
concurrency:
group: deploy-vps
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
command_timeout: 30m
script: |
set -e
if [ ! -d ~/imphnen-frontend-service-hackathon ]; then
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-hackathon
else
cd ~/imphnen-frontend-service-hackathon && git fetch origin && git reset --hard origin/develop && git pull
fi
cat > ~/imphnen-frontend-service-hackathon/.env.local << EOF
VITE_API_URL=${{ secrets.VITE_API_URL }}
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
EOF
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml down || true
docker builder prune -af || true
DOCKER_BUILDKIT=1 docker compose --env-file ~/imphnen-frontend-service-hackathon/.env.local -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml build --progress=plain
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml up -d
docker image prune -af
+3 -12
View File
@@ -7,10 +7,6 @@ on:
paths:
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
concurrency:
group: deploy-vps
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
@@ -25,20 +21,15 @@ jobs:
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
command_timeout: 30m
script: |
set -e
if [ ! -d ~/imphnen-frontend-service ]; then
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service
else
cd ~/imphnen-frontend-service && git fetch origin && git reset --hard origin/develop && git pull
cd ~/imphnen-frontend-service && git pull
fi
cat > ~/imphnen-frontend-service/apps/landing/.env << EOF
NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}
NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
EOF
echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" > ~/imphnen-frontend-service/apps/landing/.env
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml down || true
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build --progress=plain
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d
docker image prune -af
-4
View File
@@ -62,7 +62,3 @@ storybook-static
# Next.js
.next
out
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
.env.local
-1
View File
@@ -1 +0,0 @@
Tue, Nov 25, 2025 4:21:27 PM
-2
View File
@@ -1,2 +0,0 @@
# Deployment trigger
# Updated to deploy circular dependency fixes and auth hooks
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,52 +0,0 @@
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
import { PieLabelProps } from "recharts/types/polar/Pie"
type ChartProps = {
name: string
value: number
color: string
}
const chartData: ChartProps[] = [
{ name: "Active", value: 49, color: "#23A1EB" },
{ name: "Done", value: 24, color: "#81CBF8" },
{ name: "Canceled", value: 27, color: "#BCE1FB" },
]
const RADIAN = Math.PI / 180;
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent }: PieLabelProps) => {
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
const x = cx + radius * Math.cos(-(midAngle ?? 0) * RADIAN);
const y = cy + radius * Math.sin(-(midAngle ?? 0) * RADIAN);
return (
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
{`${((percent ?? 1) * 100).toFixed(0)}%`}
</text>
);
};
export const SessionStatusChart = () => {
return (
<ResponsiveContainer width="100%" height={320}>
<PieChart width={500} height={320}>
<Pie
data={chartData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
innerRadius={40}
outerRadius={100}
labelLine={false}
label={renderCustomizedLabel}
>
{chartData.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.color} />
))}
</Pie>
<Tooltip />
</PieChart>
</ResponsiveContainer>
)
}
@@ -1,36 +0,0 @@
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
type ChartProps = {
name: string
activeUser: number
activeSession: number
}
const chartData: ChartProps[] = [
{ name: "2014", activeUser: 0, activeSession: 0 },
{ name: "2015", activeUser: 15, activeSession: 25 },
{ name: "2016", activeUser: 30, activeSession: 40 },
{ name: "2017", activeUser: 45, activeSession: 55 },
{ name: "2018", activeUser: 60, activeSession: 70 },
{ name: "2019", activeUser: 75, activeSession: 85 },
{ name: "2020", activeUser: 90, activeSession: 95 },
{ name: "2021", activeUser: 85, activeSession: 80 },
{ name: "2022", activeUser: 95, activeSession: 90 },
{ name: "2023", activeUser: 100, activeSession: 100 },
]
export const UserGrowthChart = () => {
return (
<ResponsiveContainer width="100%" height={320}>
<LineChart data={chartData} width={500} height={320} margin={{ left: -32 }}>
<CartesianGrid />
<XAxis dataKey="name" />
<YAxis tickCount={10} />
<Tooltip />
<Legend />
<Line dataKey="activeUser" stroke="#23A1EB" />
<Line dataKey="activeSession" stroke="#0877C1" />
</LineChart>
</ResponsiveContainer>
)
}
@@ -1,115 +0,0 @@
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms";
import { For } from "@imphnen-frontend-service/utils";
import { ReactElement } from "react";
import { UserGrowthChart } from "./_components/chart/user-growth";
import { SessionStatusChart } from "./_components/chart/session-status";
const Overview = () => {
return (
<div className="bg-white px-6 py-4 rounded-md shadow">
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
<p className="text-neutral-400 text-p3">Total Users</p>
</div>
)
}
export default function Components(): ReactElement {
return (
<BackofficeWrapper title="Dimentorin.dev">
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
<div className="space-y-14">
<div>
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
Overview
</Button>
<div className="grid grid-cols-5 gap-5">
<For data={Array.from({ length: 5 })}>
{(_, index) => <Overview key={index} />}
</For>
</div>
</div>
<div>
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
Trends & Analytics
</Button>
<div className="grid grid-cols-7 gap-x-5">
<div className="bg-white px-6 py-4 rounded-lg col-span-5">
<div className="flex items-center justify-between mb-7">
<h2 className="font-semibold text-p3 text-neutral-700">User Growth</h2>
<div></div>
</div>
<UserGrowthChart />
</div>
<div className="bg-white px-6 py-4 rounded-lg col-span-2">
<h2 className="font-semibold text-p3 text-neutral-700 mb-7">Session Status</h2>
<SessionStatusChart />
</div>
</div>
</div>
<div className="grid grid-cols-2 gap-x-5">
<div className="bg-white px-7 py-4 rounded-lg">
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top 5 Mentors</h2>
<div>
<table className="w-full">
<thead>
<tr className="text-label1 bg-primary-50 text-left rounded-full">
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
<th className="font-medium py-4 px-5 rounded-r-lg">Avg Rating</th>
</tr>
</thead>
<tbody>
<For data={Array.from({ length: 5 })}>
{(_, index) => (
<tr key={index} className="shadow rounded-lg">
<td className="py-4 px-5">{index + 1}</td>
<td className="py-4 px-5">Mursid Al-Catraz</td>
<td className="py-4 px-5">4.9</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
<div className="bg-white px-6 py-4 rounded-lg">
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top Booked Mentoring Topics</h2>
<div>
<table className="w-full">
<thead>
<tr className="text-label1 bg-primary-50 text-left font-medium">
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
</tr>
</thead>
<tbody>
<For data={Array.from({ length: 5 })}>
{(_, index) => (
<tr key={index} className="shadow rounded-lg">
<td className="py-4 px-5">{index + 1}</td>
<td className="py-4 px-5">Mursid Al-Catraz</td>
<td className="py-4 px-5">1000</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
</div>
</div>
</div>
</BackofficeWrapper>
)
}
@@ -1,181 +0,0 @@
import { SearchOutlined } from "@ant-design/icons";
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
import { cn, For } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { ReactElement, useState } from "react"
const TABS = {
MENTORING: 'Mentoring',
PLATFORM: 'Platform'
} as const
type Tabs = typeof TABS[keyof typeof TABS]
type FeedbackStatus = 'done' | 'todo';
interface FeedbackType {
id: number
name: string
email: string
rating: number
status: FeedbackStatus
}
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
email: 'fullname23@gmail.com',
rating: 4.5,
status: i % 2 === 0 ? 'done' : 'todo',
}))
export default function Components(): ReactElement {
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const columns: ColumnDef<FeedbackType>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'name',
header: 'Name',
accessorKey: 'name',
},
{
id: 'email',
header: 'Email',
accessorKey: 'email',
},
{
id: 'rating',
header: 'Rating',
accessorKey: 'rating',
},
{
id: 'status',
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<FeedbackStatus, string> = {
done: 'bg-success-200 text-success-500',
todo: 'bg-primary-200 text-primary-500',
};
const statusText: Record<FeedbackStatus, string> = {
done: 'Done',
todo: 'To Do',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
meta: { cellClassName: cn("w-72") },
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation()
}}
className="flex items-center gap-2 w-max"
>
<SearchOutlined className="text-[16px]" /> Lihat Feedback
</Button>
),
},
]
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
});
return (
<BackofficeWrapper title="Dimentorin.dev">
<div className="mb-8 flex justify-between items-center">
<h1 className="text-p1 font-semibold text-neutral-700">Feedback</h1>
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
<For data={Object.values(TABS)}>
{(tab) => (
<Button
key={tab}
variant="text"
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
onClick={() => setActiveTab(tab)}
>
{tab}
</Button>
)}
</For>
</div>
</div>
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
<div className="flex justify-between items-center gap-5 mb-2">
<div className="relative w-full">
<Input
placeholder="Cari berdasarkan nama mentor/mentee"
className="pl-12 w-full max-h-full"
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>Rating</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
</section>
</BackofficeWrapper>
);
}
@@ -7,7 +7,7 @@ export const AppLayout: FC = (): ReactElement => {
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto">
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
@@ -1,85 +0,0 @@
import { ArrowRightOutlined } from "@ant-design/icons";
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
import { Accordion, Modal, ModalProps } from "@imphnen-frontend-service/ui/molecules";
import { cn, For } from "@imphnen-frontend-service/utils";
import { FC } from "react";
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
export const ModalCreateRoadmap: FC<Omit<ModalProps, 'children'>> = ({ isOpen, onClose }) => {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
className="xl:max-w-[64rem] bg-white px-10 py-9"
closeButtonClassName="hidden"
>
<div className="overflow-y-auto max-h-[80vh]">
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Create Roadmaps
</h1>
<div className="grid grid-cols-5 gap-6 mb-12">
<div className="col-span-3">
<label className={labelClass}>Prompt</label>
<Textarea
className="min-w-full w-full h-[calc(100%-2rem)]"
placeholder="Create a learning roadmap for (subject) at the (level) level. Include topics, estimated duration, and logical progression."
/>
</div>
<div className="col-span-2 space-y-8">
<div>
<label className={labelClass}>Roadmap Name</label>
<Input type="text" className="min-w-full w-full" placeholder="Nama Roadmap" />
</div>
<div>
<label className={labelClass}>Roadmap Name</label>
<Select className="w-full">
<option value="pemula">Pemula</option>
<option value="menengah">Menengah</option>
</Select>
</div>
<div>
<label className={labelClass}>Model</label>
<Select className="w-full">
<option value="gpt-3.5-turbo">GPT 3.5 Turbo</option>
<option value="gpt-4">GPT 4</option>
<option value="gpt-4-32k">GPT 4 32k</option>
</Select>
</div>
</div>
<div className="col-span-full">
<Button>
Generate Roadmap
</Button>
</div>
</div>
<div>
<h2 className="text-neutral-600 text-p2 font-semibold mb-8">
Roadmap Preview
</h2>
<div className="space-y-2.5">
<For data={Array.from({ length: 3 })}>
{(_, index) => (
<Accordion
key={index}
title={`Day ${index + 1}`}
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. In convallis tincidunt nisl, id consequat mi malesuada vel. Nulla facilisi. Nam in turpis ligula."
/>
)}
</For>
</div>
</div>
<div className="flex justify-end mt-12">
<Button type="button" className="flex items-center gap-5">
Submit Roadmap
<ArrowRightOutlined />
</Button>
</div>
</div>
</Modal>
)
}
@@ -1,174 +0,0 @@
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons";
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
import { cn } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { useState } from "react";
import { ModalCreateRoadmap } from "./_components/modal/create-roadmap";
type LearningStatus = 'active' | 'inactive'
type RoadmapType = {
id: number
name: string
learningLevel: string
status: LearningStatus
}
const mockData: RoadmapType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i === 0 ? 'Ahmad Wijuana' : 'Anna Wiguana',
learningLevel: ['Pemula', 'Menengah'][Math.floor(Math.random() * 2)],
status: i % 2 === 0 ? 'active' : 'inactive',
}))
export default function Components(): React.ReactElement {
const [openCreateModal, setOpenCreateModal] = useState(false)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const columns: ColumnDef<RoadmapType>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'id',
header: 'No',
accessorKey: 'id',
},
{
id: 'name',
header: 'Nama Roadmap',
accessorKey: 'name',
},
{
id: 'learningLevel',
header: 'Tingkat Belajar',
accessorKey: 'learningLevel',
},
{
id: 'status',
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<LearningStatus, string> = {
inactive: 'bg-danger-200 text-danger-700',
active: 'bg-success-200 text-success-500',
};
const statusText: Record<LearningStatus, string> = {
inactive: 'Inactive',
active: 'Active',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
meta: { cellClassName: cn("w-72") },
cell: ({ row }) => (
<div className="flex gap-[8px]">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
}}
className="flex items-center gap-2"
>
<EditOutlined /> Action
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation();
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
</div>
),
},
]
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
});
return (
<BackofficeWrapper title="Dimentorin.dev">
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Content & Roadmap</h1>
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
<div className="flex items-center justify-between mb-9">
<h2 className="text-p2 font-semibold text-neutral-600">AI Roadmaps</h2>
<Button type="button" variant="primary" className="flex items-center gap-2" onClick={() => setOpenCreateModal(true)}>
<PlusOutlined /> Buat Roadmap
</Button>
</div>
<div className="flex justify-between items-center gap-5 mb-2">
<div className="relative w-full">
<Input
placeholder="Cari berdasarkan nama roadmap"
className="pl-12 w-full max-h-full"
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>Tingkat Belajar</option>
<option value="pemula">Pemula</option>
<option value="menengah">Menengah</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
</section>
<ModalCreateRoadmap isOpen={openCreateModal} onClose={() => setOpenCreateModal(false)} />
</BackofficeWrapper>
)
}
@@ -1,114 +0,0 @@
import { Icon } from "@iconify/react";
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
import { Modal } from "@imphnen-frontend-service/ui/molecules";
import { cn, For } from "@imphnen-frontend-service/utils";
import { FC } from "react";
const TOPICS = [
{ id: 2, icon: '🏢', name: 'Industry Insight' },
{ id: 4, icon: '🖥️', name: 'Basic IT' },
]
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
Saya ingin tahu secara khusus tentang ___.
1.Pertanyaan Anda
2. ...
3. ...`
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
type ModalProps = {
open: boolean
setOpen: (open: boolean) => void
}
export const ModalDetailSession: FC<ModalProps> = ({ open, setOpen }) => {
return (
<Modal
isOpen={open}
onClose={() => setOpen(false)}
className="xl:max-w-[64rem] bg-white px-10 py-9"
closeButtonClassName="hidden"
>
<div>
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Detail Sesi
</h1>
<div className="grid grid-cols-9 px-9 py-7 border rounded-md gap-12 mb-10">
<div className="col-span-4 flex items-center gap-x-12">
<div>
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentor</h2>
<div>
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
</div>
</div>
<Icon icon="ph:arrow-right" className="text-9xl text-primary-500" />
</div>
<div className="col-span-5 flex items-center gap-12">
<div>
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentee</h2>
<div>
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
</div>
</div>
<div>
<h2 className="text-p2 font-semibold text-neutral-700 mb-4">Status</h2>
<div className="py-2 px-6 rounded-md text-center bg-success-200 text-success-500 font-semibold">
Finished
</div>
</div>
</div>
</div>
<div>
<h1 className="text-p3 font-medium mb-2.5">Topics</h1>
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5 mb-8">
<For data={TOPICS}>
{(item, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow font-medium"
>
<span>{item.icon} </span>
<span>{item.name}</span>
</div>
)}
</For>
</div>
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
<div>
<label className={labelClass}>Tanggal</label>
<Input type="date" className="min-w-full w-full" />
</div>
<div>
<label className={labelClass}>Waktu</label>
<Input type="time" className="min-w-full w-full" />
</div>
<div className="relative md:col-span-full">
<label className={labelClass}>Lokasi</label>
<Select className="min-w-full w-full">
<option value="online">Online</option>
<option value="offline">Offline</option>
</Select>
</div>
<div className="md:col-span-full">
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
<Textarea
className="min-w-full w-full h-40"
placeholder={placeholder}
/>
</div>
</div>
</div>
</div>
</Modal>
)
}
@@ -1,168 +0,0 @@
import { SearchOutlined } from "@ant-design/icons";
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
import { cn } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { ReactElement, useState } from "react";
import { ModalDetailSession } from "./_components/modal/detail";
type SessionStatus = 'ongoing' | 'finished';
interface SessionType {
id: string
mentorName: string
menteeName: string
datetime: number
status: SessionStatus
}
const mockData: SessionType[] = Array.from({ length: 90 }, (_, i) => ({
id: `DS-${i + 1}`,
mentorName: 'Ahmad Wijuana',
menteeName: 'Sofia Wijuana',
datetime: new Date().getTime(),
status: i % 2 === 0 ? 'ongoing' : 'finished',
}))
export default function Components(): ReactElement {
const [openDetail, setOpenDetail] = useState(false);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const columns: ColumnDef<SessionType>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'id',
header: 'ID Sesi',
accessorKey: 'id',
},
{
id: 'mentorName',
header: 'Nama Mentor',
accessorKey: 'name',
},
{
id: 'menteeName',
header: 'Nama Mentee',
accessorKey: 'name',
},
{
id: 'datetime',
header: 'Waktu',
accessorKey: 'datetime',
},
{
id: 'status',
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<SessionStatus, string> = {
ongoing: 'bg-warning-200 text-warning-700',
finished: 'bg-success-200 text-success-500',
};
const statusText: Record<SessionStatus, string> = {
ongoing: 'On Going',
finished: 'Finished',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
meta: { cellClassName: cn("w-52") },
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setOpenDetail(true);
}}
className="flex items-center gap-2 w-max"
>
<SearchOutlined className="text-[16px]" /> Cek Detail
</Button>
),
},
]
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
});
return (
<BackofficeWrapper title="Dimentorin.dev">
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Session Management</h1>
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
<div className="flex justify-between items-center gap-5 mb-2">
<div className="relative w-full">
<Input
placeholder="Cari berdasarkan nama lengkap"
className="pl-12 w-full max-h-full"
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>Rating</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>Status</option>
<option value="finished">Finished</option>
<option value="ongoing">On Going</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
</section>
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
</BackofficeWrapper>
)
}
@@ -1,61 +0,0 @@
import { Button, Input, Select, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
import { cn } from "@imphnen-frontend-service/utils"
import { FC } from "react"
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
export const GeneralSettings: FC = () => {
return (
<div>
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">General Settings</h1>
<div>
<ToggleInput label="Mode Maintenance" />
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Platform Settings</h2>
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
<div>
<label className={labelClass}>Nama Platform</label>
<Input type="text" className="min-w-full w-full" />
</div>
<div>
<label className={labelClass}>Bahasa</label>
<Select defaultValue="id" className="w-full">
<option value="id">Indonesia</option>
<option value="en">English</option>
</Select>
</div>
<div>
<label className={labelClass}>Logo Platform</label>
<Input type="file" className="min-w-full w-full" />
</div>
<div>
<label className={labelClass}>Favicon</label>
<Input type="file" className="min-w-full w-full" />
</div>
</div>
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Legal Settings</h2>
<div className="grid gap-x-8 gap-y-5 mb-20">
<div>
<label className={labelClass}>URL Syarat & Ketentuan</label>
<Input type="text" className="min-w-full w-full" />
</div>
<div>
<label className={labelClass}>URL Kebijakan Privasi</label>
<Input type="text" className="min-w-full w-full" />
</div>
</div>
<div className="flex justify-end gap-5">
<Button type="button" variant="bordered">
Batal
</Button>
<Button type="button">
Simpan
</Button>
</div>
</div>
</div>
)
}
@@ -1,34 +0,0 @@
import { Button, Textarea, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
import { FC } from "react"
export const NotificationSettings: FC = () => {
return (
<div>
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Notification Settings</h1>
<div>
<div className="flex items-center gap-8 flex-wrap">
<ToggleInput label="Nyalakan Notifikasi Email" />
<ToggleInput label="Beritahu mentor ketika ada request" />
<ToggleInput label="Beritahu mentee untuk update sesi " />
</div>
<div className="mb-20">
<label className="text-neutral-800 font-medium inline-block mb-2 text-p3">
API Integrasi Notifikasi (URL)
</label>
<Textarea placeholder="Masukkan url API notifikasi yang akan digunakan" className="w-full h-40" />
</div>
<div className="flex justify-end gap-5">
<Button type="button" variant="bordered">
Batal
</Button>
<Button type="button">
Simpan
</Button>
</div>
</div>
</div>
)
}
@@ -1,50 +0,0 @@
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
import { cn } from "@imphnen-frontend-service/utils"
import { FC } from "react"
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
export const PaymentSettings: FC = () => {
return (
<div>
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Payment</h1>
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
<div>
<label className={labelClass}>Integrasi Payment Gateway</label>
<Textarea className="min-w-full w-full h-20" placeholder="Durasi dalam menit" />
</div>
<div>
<label className={labelClass}>Mata Uang</label>
<Select defaultValue="idr">
<option value="idr">Rupiah (IDR)</option>
<option value="usd">Dollar (USD)</option>
</Select>
</div>
<div>
<label className={labelClass}>Harga sesi mentoring <span className="text-neutral-600">(default)</span></label>
<Input type="text" className="min-w-full w-full" placeholder="Masukkan harga sesi mentoring" />
</div>
<div>
<label className={labelClass}>Tarif Komisi untuk Platform</label>
<Input type="text" className="min-w-full w-full" placeholder="Masukkan persentase komisi" />
</div>
</div>
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Invoice</h2>
<div className="mb-32">
<label className={labelClass}>Masukkan Format Invoice</label>
<Input type="file" className="min-w-full w-full" placeholder="InvoiceDimentorin.png" />
</div>
<div className="flex justify-end gap-5">
<Button type="button" variant="bordered">
Batal
</Button>
<Button type="button">
Simpan
</Button>
</div>
</div>
)
}
@@ -1,35 +0,0 @@
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
import { cn } from "@imphnen-frontend-service/utils";
import { FC } from "react";
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
export const SecuritySettings: FC = () => {
return (
<div>
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Security Settings</h1>
<div className="grid grid-cols-2 gap-8 mb-20">
<div>
<label className={labelClass}>Durasi Session Timeout</label>
<Input type="text" className="min-w-full w-full" placeholder="Durasi dalam menit" />
<p className="text-[10px] text-neutral-800">Auto logout setelah X menit tidak aktif</p>
</div>
<div>
<label className={labelClass}>Blokir Setelah Upaya Gagal</label>
<Input type="text" className="min-w-full w-full" placeholder="x kali perobaan login" />
<p className="text-[10px] text-neutral-800">Misal: 5 kali salah login = lock akun</p>
</div>
</div>
<div className="flex justify-end gap-5">
<Button type="button" variant="bordered">
Batal
</Button>
<Button type="button">
Simpan
</Button>
</div>
</div>
)
}
@@ -1,118 +0,0 @@
import { DeleteOutlined, UserSwitchOutlined } from "@ant-design/icons";
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { DataTable } from "@imphnen-frontend-service/ui/organisms";
import { cn } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { FC, useState } from "react";
type UserRolesPermissionType = {
id: number
role: string
totalUser: number
}
const mockData: UserRolesPermissionType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
role: ['Admin', 'Super Admin', 'Mentee', 'Mentor'][Math.floor(Math.random() * 4)],
totalUser: 10
}))
export const UserRolesPermission: FC = () => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const columns: ColumnDef<UserRolesPermissionType>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'role',
header: 'Role',
accessorKey: 'role',
},
{
id: 'totalUser',
header: 'Total User',
accessorKey: 'totalUser',
},
{
header: 'Action',
meta: { cellClassName: cn("w-96") },
cell: ({ row }) => (
<div className="flex items-center gap-4">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
}}
className="flex items-center gap-2 w-max"
>
<UserSwitchOutlined className="text-[16px]" /> Manage Permissions
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation();
}}
className="flex items-center gap-2 w-max"
>
<DeleteOutlined className="text-[16px]" /> Delete Role
</Button>
</div>
),
},
]
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
});
return (
<div>
<div className="mb-8 flex items-center justify-between">
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
<Button type="button">
Add Rols
</Button>
</div>
<div className="bg-white shadow p-8 rounded-lg">
<DataTable data={mockData} columns={columns} table={table} />
</div>
</div>
)
}
@@ -1,53 +0,0 @@
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms"
import { cn, For } from "@imphnen-frontend-service/utils"
import { useState } from "react"
import { GeneralSettings } from "./_components/general"
import { UserRolesPermission } from "./_components/user-roles-permission"
import { NotificationSettings } from "./_components/notification"
import { SecuritySettings } from "./_components/security"
import { PaymentSettings } from "./_components/payment"
const TABS = {
general: "General Settings",
userRolePermissions: "User Roles & Permissions",
notification: "Notification Settings",
security: "Security",
payment: "Payment",
} as const
type Tabs = typeof TABS[keyof typeof TABS]
export default function Components(): React.ReactElement {
const [activeTab, setActiveTab] = useState<Tabs>(TABS.general)
return (
<BackofficeWrapper title="Dimentorin.dev">
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Settings</h1>
<div className="flex items-start gap-x-8">
<div className="w-64 bg-white p-2.5 shadow space-y-2 rounded-md">
<For data={Object.values(TABS)}>
{(tab) => (
<button
key={tab}
className={cn(
"px-4 py-3 w-full text-left font-medium rounded-md text-neutral-400 cursor-pointer select-none hover:bg-primary-100",
activeTab === tab && "bg-primary-500 text-white hover:bg-primary-600"
)}
onClick={() => setActiveTab(tab)}
>
{tab}
</button>
)}
</For>
</div>
<div className="bg-white px-8 py-6 shadow space-y-2 rounded-md flex-1">
{activeTab === TABS.general && <GeneralSettings />}
{activeTab === TABS.userRolePermissions && <UserRolesPermission />}
{activeTab === TABS.notification && <NotificationSettings />}
{activeTab === TABS.security && <SecuritySettings />}
{activeTab === TABS.payment && <PaymentSettings />}
</div>
</div>
</BackofficeWrapper>
)
}
@@ -1,55 +0,0 @@
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { Modal } from "@imphnen-frontend-service/ui/molecules";
import { FC } from "react";
type ModalDeleteProps = {
open: boolean
setOpen: (open: boolean) => void
hadnleDelete?: () => void
}
export const ModalDelete: FC<ModalDeleteProps> = ({ open, setOpen, hadnleDelete }) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={open}
onClose={() => setOpen(false)}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete item?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Hapus Akun
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus akun ini?
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full border-danger-500 text-danger-500 hover:bg-danger-50 hover:text-danger-600 hover:border-danger-600"
onClick={() => setOpen(false)}
>
Batal
</Button>
<Button
variant="danger"
size="lg"
className="w-full"
onClick={() => hadnleDelete?.()}
>
Hapus Akun
</Button>
</Modal.Content>
</Modal>
)
}
@@ -1,86 +0,0 @@
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
import { cn } from "@imphnen-frontend-service/utils";
import { FC, useState } from "react";
import { ModalDelete } from "../delete";
import { ModalProps } from "../type";
import { ModalSuspendOrBan } from "../suspend-or-ban";
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
export const AccountProfile: FC<ModalProps> = ({ setOpen }) => {
const [openDelete, setOpenDelete] = useState(false)
const [openSuspendOrBan, setOpenSuspendOrBan] = useState(false)
return (
<div>
<div className="flex items-center gap-x-8 mb-8">
<div className="size-[100px] rounded-full overflow-hidden">
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
</div>
<div>
<Button type="button" size="sm" variant="bordered" className="mb-4">
Upload Foto
</Button>
<p className="text-neutral-400 font-medium">
Setidaknya rekomendasi ukuran 240x240 px. <br />
.jpg, .jpeg, .png diperbolehkan
</p>
</div>
</div>
<div>
<div className="mb-8">
<h1 className="mb-7 text-p2 font-semibold">Informasi Pribadi</h1>
<div className="grid grid-cols-2 gap-8">
<div>
<label className={labelClass}>Nama Depan</label>
<Input type="text" className="min-w-full w-full" placeholder="Nama Depan" />
</div>
<div>
<label className={labelClass}>Nama Belakang</label>
<Input type="text" className="min-w-full w-full" placeholder="Nama Belakang" />
</div>
<div>
<label className={labelClass}>Email</label>
<Input type="email" className="min-w-full w-full" placeholder="Email" />
</div>
<div>
<label className={labelClass}>Nomor Telepon</label>
<Input type="text" className="min-w-full w-full" placeholder="+62 81234567890" />
</div>
</div>
</div>
<div>
<h1 className="mb-7 text-p2 font-semibold">Status Akun</h1>
<div className="flex items-center gap-5">
<Button
type="button"
size="sm"
variant="bordered"
className="border-danger-500 text-danger-500"
onClick={() => setOpenSuspendOrBan(true)}
>
Suspend/Ban
</Button>
<Button type="button" size="sm" variant="danger" onClick={() => setOpenDelete(true)}>
Delete Akun
</Button>
</div>
</div>
<div className="flex justify-end gap-x-5">
<Button type="button" variant="bordered" onClick={() => setOpen(false)}>
Batal
</Button>
<Button type="button" disabled>
Simpan
</Button>
</div>
</div>
<ModalDelete open={openDelete} setOpen={setOpenDelete} />
<ModalSuspendOrBan open={openSuspendOrBan} setOpen={setOpenSuspendOrBan} />
</div>
)
}
@@ -1,110 +0,0 @@
import { SearchOutlined } from "@ant-design/icons"
import { Input, Select } from "@imphnen-frontend-service/ui/atoms"
import { DataTable } from "@imphnen-frontend-service/ui/organisms"
import { cn } from "@imphnen-frontend-service/utils"
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"
import { FC, useState } from "react"
import dayjs from "dayjs"
interface ActivityLogType {
id: number
date: string
menu: string
activity: string
}
const mockData: ActivityLogType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
date: new Date().toISOString(),
menu: 'Mentoring',
activity: 'Mentoring Session',
}))
export const ActivityLog: FC = () => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
})
const columns: ColumnDef<ActivityLogType>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-16") },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'date',
header: 'Waktu',
accessorKey: 'date',
cell: (info) => dayjs(info.row.original.date).format('DD MMMM YYYY, HH:mm WIB'),
},
{
id: 'menu',
header: 'Menu',
accessorKey: 'menu',
},
{
id: 'activity',
header: 'Activity',
accessorKey: 'activity',
}
]
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
});
return (
<div className="p-8 shadow rounded-lg">
<div className="flex justify-between items-center gap-5 mb-9">
<div className="relative w-1/2">
<Input
placeholder="Cari aktivitas"
className="pl-12 w-full max-h-full"
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<div className="w-1/4">
<Input type="date" className="min-w-full w-full" />
</div>
<Select>
<option selected disabled>Menu</option>
<option value="profle">Profile</option>
<option value="profle-2">Profile</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
</div>
)
}
@@ -1,89 +0,0 @@
import { Icon } from "@iconify/react"
import { Button } from "@imphnen-frontend-service/ui/atoms"
import { For } from "@imphnen-frontend-service/utils"
import { FC } from "react"
const SOCIAL_LINKS = [
{ icon: <Icon icon="mdi:linkedin" className="text-2xl" />, url: "https://linkedin.com", label: "LinkedIn" },
{ icon: <Icon icon="mdi:github" className="text-2xl" />, url: "https://github.com", label: "Github" },
{ icon: <Icon icon="mingcute:meta-line" className="text-2xl" />, url: "https://facebook.com", label: "Facebook (Meta)" },
{ icon: <Icon icon="mdi:stack-overflow" className="text-2xl" />, url: "https://stackoverflow.com", label: "Stack Overflow" }
]
export const DetailProfile: FC = () => {
return (
<div>
<div className="shadow rounded-lg p-8 mb-7">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-x-6">
<div className="size-[54px] rounded-full overflow-hidden">
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
</div>
<div>
<h1 className="text-p2 font-semibold text-neutral-800">Muhammad Firdaus Oiwobo</h1>
<p className="text-p3 font-medium text-neutral-600">Mentor</p>
</div>
</div>
<Button type="button" variant="text" className="bg-primary-100">
Actively Seeking Job
</Button>
</div>
<div className="flex items-center gap-5">
<For data={SOCIAL_LINKS}>
{({ icon, url, label }) => (
<a key={url} href={url} target="_blank" rel="noreferrer">
<Button type="button" size="sm" className="flex items-center gap-x-2">
{icon}
{label}
</Button>
</a>
)}
</For>
</div>
</div>
<div className="grid grid-cols-2 gap-7">
<div className="text-pretty px-8 py-10 rounded-lg shadow h-max">
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Description</h1>
<p className="text-p3 font-medium text-neutral-600">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placeLine truncated
</p>
</div>
<div className="px-8 py-10 rounded-lg shadow h-max">
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Personal Informations</h1>
<div className="grid gap-6">
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="ic:outline-mail" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">rzalaxib23@gmail.com</p>
<p className="text-neutral-600 font-medium">Email Address</p>
</div>
</div>
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="cil:phone" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">+62 888 8888 8888</p>
<p className="text-neutral-600 font-medium">Phone Number</p>
</div>
</div>
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="ion:location-outline" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">Jl. Mergosari, Kec. Suryakencana, Banjaran</p>
<p className="text-neutral-600 font-medium">Location</p>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -1,68 +0,0 @@
import { Button } from "@imphnen-frontend-service/ui/atoms"
import { Modal } from "@imphnen-frontend-service/ui/molecules"
import { cn, For, Show } from "@imphnen-frontend-service/utils"
import { FC, useEffect, useState } from "react"
import { AccountProfile } from "./account-profile"
import { DetailProfile } from "./detail-profile"
import { ActivityLog } from "./activity-log"
import { ModalDetailUserProps } from "./type"
const TABS = {
account: 'account profile',
detail: 'detail profile',
activity: 'activity logs',
} as const
type TabType = typeof TABS[keyof typeof TABS]
export const ModalDetailUser: FC<ModalDetailUserProps> = ({
open,
setOpen,
}) => {
const [activeTab, setActiveTab] = useState<TabType>(TABS.account)
useEffect(() => {
if (!open) setActiveTab(TABS.account)
}, [open])
return (
<Modal
isOpen={open}
onClose={() => setOpen(false)}
className="xl:max-w-[84rem] bg-white px-10 py-9"
closeButtonClassName="hidden"
>
<div>
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Detail - Muhammad Firdaus Oiwobo
</h1>
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md w-max mb-8">
<For data={Object.values(TABS)}>
{(tab) => (
<Button
key={tab}
variant="text"
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
onClick={() => setActiveTab(tab)}
>
{tab}
</Button>
)}
</For>
</div>
<div>
<Show condition={activeTab === TABS.account}>
<AccountProfile open={open} setOpen={setOpen} />
</Show>
<Show condition={activeTab === TABS.detail}>
<DetailProfile />
</Show>
<Show condition={activeTab === TABS.activity}>
<ActivityLog />
</Show>
</div>
</div>
</Modal>
)
}
@@ -1,5 +0,0 @@
import { ModalProps } from "../type";
export interface ModalDetailUserProps extends ModalProps {
userId?: number | null
}
@@ -1,42 +0,0 @@
import { FC } from "react";
import { ModalProps } from "../type";
import { Modal } from "@imphnen-frontend-service/ui/molecules";
import { cn } from "@imphnen-frontend-service/utils";
import { Button, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
export const ModalSuspendOrBan: FC<ModalProps> = ({ open, setOpen }) => {
return (
<Modal
isOpen={open}
onClose={() => setOpen(false)}
className="bg-white px-10 py-9 xl:max-w-[32rem]"
closeButtonClassName="hidden"
>
<div>
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Suspend/Ban
</h1>
<div className="space-y-6">
<div>
<label className={labelClass}>Suspend/Ban</label>
<Select defaultValue="suspend" className="w-full">
<option value="suspend">Suspend</option>
<option value="banned">Banned</option>
</Select>
</div>
<div>
<label className={labelClass}>Alasan</label>
<Textarea placeholder="Masukkan alasan suspend/ban" className="w-full h-40" />
</div>
<Button type="button" className="w-full">
Selesai
</Button>
</div>
</div>
</Modal>
)
}
@@ -1,4 +0,0 @@
export interface ModalProps {
open: boolean
setOpen: (open: boolean) => void
}
@@ -1,187 +0,0 @@
import { SearchOutlined } from "@ant-design/icons";
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
import { cn, For } from "@imphnen-frontend-service/utils";
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
import { ReactElement, useState } from "react";
import { ModalDetailUser } from "./_components/modal/detail";
type UserStatus = 'active' | 'inactive';
interface UserType {
id: number
name: string
email: string
rating: number
status: UserStatus
}
const mockData: UserType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
email: 'fullname23@gmail.com',
rating: 4.5,
status: i % 2 === 0 ? 'active' : 'inactive',
}))
export default function Components(): ReactElement {
const TABS = ['mentor', 'mentee'] as const
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor')
const [showDetail, setShowDetail] = useState(false)
const [selectedUserId, setSelectedUserId] = useState<number | null>(null)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
});
const columns: ColumnDef<UserType>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-20") },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'name',
header: 'Name',
accessorKey: 'name',
},
{
id: 'email',
header: 'Email',
accessorKey: 'email',
},
{
id: 'rating',
header: 'Rating',
accessorKey: 'rating',
},
{
id: 'status',
header: 'Status',
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status;
const statusColors: Record<UserStatus, string> = {
active: 'bg-success-200 text-success-500',
inactive: 'bg-danger-200 text-danger-500',
};
const statusText: Record<UserStatus, string> = {
active: 'Active',
inactive: 'Inactive',
};
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
</div>
);
},
},
{
header: 'Action',
meta: { cellClassName: cn("w-72") },
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation();
setSelectedUserId(row.original.id);
setShowDetail(true);
}}
className="flex items-center gap-2 w-max"
>
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
</Button>
),
},
]
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
});
return (
<BackofficeWrapper title="Dimentorin.dev">
<div className="mb-8 flex justify-between items-center">
<h1 className="text-p1 font-semibold text-neutral-700">User Management</h1>
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
<For data={TABS}>
{(tab) => (
<Button
key={tab}
variant="text"
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
onClick={() => setActiveTab(tab)}
>
{tab}
</Button>
)}
</For>
</div>
</div>
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
<div className="flex justify-between items-center gap-5 mb-2">
<div className="relative w-full">
<Input
placeholder="Cari berdasarkan nama lengkap"
className="pl-12 w-full max-h-full"
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>Rating</option>
<option value="4.5">4.5</option>
<option value="5">5</option>
</Select>
<Select>
<option selected disabled>Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
</section>
<ModalDetailUser
open={showDetail}
setOpen={setShowDetail}
userId={selectedUserId}
/>
</BackofficeWrapper>
);
}
-1
View File
@@ -1 +0,0 @@
Tue, Nov 25, 2025 4:21:27 PM
-2
View File
@@ -1,2 +0,0 @@
# Deployment trigger
# Updated to deploy circular dependency fixes and auth hooks
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

@@ -1,166 +0,0 @@
'use client';
import { FC, ReactElement, useState } from 'react';
import { useParams } from 'react-router-dom';
import { ProfileForm, ProfileSidebar, ProfileHeader } from '../_components';
import { ArrowLeftOutlined } from '@ant-design/icons';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { NotificationModal, NotificationType } from '../_components/modals/notification-modal';
import { ProfileProvider, useProfile } from '../_components/contexts/profile-context';
import { EditProfileModal } from '../_components/modals/edit-profile-modal';
const ProfileByIdPage: FC = (): ReactElement => {
const params = useParams();
const id = (params && params.id) ? params.id as string : undefined;
if (!id) {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center">
<p className="text-red-600 text-lg">Profile ID not found.</p>
</div>
</main>
);
}
return (
<ProfileProvider profileId={id} profileType="user">
<ProfileByIdContent />
</ProfileProvider>
);
};
const ProfileByIdContent: FC = (): ReactElement => {
const { profileData, isLoading, error, profileType } = useProfile();
const [notification, setNotification] = useState<{
isOpen: boolean;
type: 'success' | 'error';
title: string;
message?: string;
}>({
isOpen: false,
type: 'success',
title: '',
message: ''
});
const [isEditProfileModalOpen, setIsEditProfileModalOpen] = useState(false);
const getProfileTitle = () => {
if (profileData?.fullname) {
return `${profileData.fullname}'s Profile`;
}
return profileType === 'user' ? 'User Profile' : 'Mentor Profile';
};
const showNotification = (type: NotificationType['type'], title: string, message?: string) => {
setNotification({
isOpen: true,
type,
title,
message
});
};
const hideNotification = () => {
setNotification(prev => ({ ...prev, isOpen: false }));
};
const openEditProfileModal = () => {
setIsEditProfileModalOpen(true);
};
const closeEditProfileModal = () => {
setIsEditProfileModalOpen(false);
};
// Set isViewOnly to true for this page
const isViewOnly = true;
if (isLoading) {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading profile...</p>
</div>
</main>
);
}
if (error) {
return (
<main className="min-h-screen flex items-center justify-center">
<div className="text-center">
<p className="text-red-600 text-lg">Failed to load profile</p>
<p className="text-gray-600 mt-2">Profile not found or you don't have permission to view it.</p>
</div>
</main>
);
}
return (
<main className="min-h-screen">
<div className="">
<div className="w-full px-8 md:px-[60px] lg:px-20 py-4">
<div className="max-w-7xl mx-auto">
<Button variant="primary" className="flex items-center gap-2">
<ArrowLeftOutlined />
Kembali ke Dashboard
</Button>
</div>
</div>
</div>
<div className="w-full px-8 md:px-[60px] lg:px-20 py-6">
<div className="max-w-7xl mx-auto">
<h1 className="text-2xl font-semibold text-gray-900">
{getProfileTitle()}
</h1>
</div>
</div>
<div className="w-full px-8 md:px-[60px] lg:px-20 pb-12">
<div className="max-w-7xl mx-auto">
<div className="grid gap-8 lg:grid-cols-12">
<div className="lg:col-span-12">
<ProfileHeader onEditProfileClick={openEditProfileModal} isViewOnly={isViewOnly} />
</div>
<div className="lg:col-span-8 order-1">
<ProfileForm
showNotification={showNotification}
isViewOnly={isViewOnly}
/>
</div>
<div className="lg:col-span-4 order-2">
<ProfileSidebar
showNotification={showNotification}
isViewOnly={isViewOnly}
/>
</div>
</div>
</div>
</div>
<NotificationModal
isOpen={notification.isOpen}
onClose={hideNotification}
type={notification.type}
title={notification.title}
message={notification.message}
header="Profile"
/>
{/* Only render EditProfileModal if not in view-only mode */}
{!isViewOnly && (
<EditProfileModal
isOpen={isEditProfileModalOpen}
onClose={closeEditProfileModal}
showNotification={showNotification}
/>
)}
</main>
);
};
export default ProfileByIdPage;
@@ -1,25 +0,0 @@
import { FC } from 'react';
import { EditOutlined } from '@ant-design/icons';
interface EditSectionButtonProps {
onClick: () => void;
disabled?: boolean;
}
export const EditSectionButton: FC<EditSectionButtonProps> = ({ onClick, disabled = false }) => {
return (
<button
onClick={onClick}
disabled={disabled}
className={`flex items-center gap-1 px-3 py-1 text-sm transition-colors duration-200 rounded-md ${
disabled
? 'text-gray-400 cursor-not-allowed opacity-50'
: 'text-[#23A1EB] hover:text-[#1e90d6] hover:bg-[#23A1EB]/10'
}`}
aria-label="Edit section"
>
<span>Edit</span>
<EditOutlined className="w-3 h-3" />
</button>
);
};
@@ -1,2 +0,0 @@
export { EditSectionButton } from './edit-section-button';
export { ModalButton } from './modal-button';
@@ -1,76 +0,0 @@
import { FC, ReactNode } from 'react';
type ButtonVariant = 'primary' | 'secondary' | 'danger';
type ButtonSize = 'sm' | 'md' | 'lg';
interface ModalButtonProps {
children: ReactNode;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
loading?: boolean;
className?: string;
}
const getVariantClasses = (variant: ButtonVariant): string => {
switch (variant) {
case 'primary':
return 'bg-[#23A1EB] hover:bg-[#1e90d6] text-white shadow-lg hover:shadow-xl';
case 'secondary':
return 'bg-white hover:bg-gray-200 text-[#23A1EB] shadow-md hover:shadow-lg';
case 'danger':
return 'bg-red-600 hover:bg-red-500 text-white shadow-lg hover:shadow-xl';
default:
return 'bg-[#23A1EB] hover:bg-[#1e90d6] text-white shadow-lg hover:shadow-xl';
}
};
const getSizeClasses = (size: ButtonSize): string => {
switch (size) {
case 'sm':
return 'px-3 py-1.5 text-sm';
case 'md':
return 'px-4 py-2 text-sm';
case 'lg':
return 'px-6 py-3 text-base';
default:
return 'px-4 py-2 text-sm';
}
};
export const ModalButton: FC<ModalButtonProps> = ({
children,
onClick,
type = 'button',
variant = 'primary',
size = 'md',
disabled = false,
loading = false,
className = '',
}) => {
const baseClasses = 'font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#23A1EB] disabled:opacity-50 disabled:cursor-not-allowed';
const variantClasses = getVariantClasses(variant);
const sizeClasses = getSizeClasses(size);
const combinedClasses = `${baseClasses} ${variantClasses} ${sizeClasses} ${className}`;
return (
<button
type={type}
onClick={onClick}
disabled={disabled || loading}
className={combinedClasses}
>
{loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
<span>Loading...</span>
</div>
) : (
children
)}
</button>
);
};
@@ -1 +0,0 @@
export * from './profile-context';
@@ -1,249 +0,0 @@
'use client';
import React, { createContext, useContext, useMemo, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import {
useAuthStore,
useUserMe,
useUserById,
useUpdateUserMe,
useUpdateUserById,
UserDetailResponseDto,
UserUpdateRequestDto,
useMentorMe,
useMentorById,
useUpdateMentorMe,
useUpdateMentorById,
MentorDetailResponseDto,
MentorUpdateRequestDto
} from '@imphnen-frontend-service/service';
type ProfileData = UserDetailResponseDto | MentorDetailResponseDto;
type ProfileUpdateData = UserUpdateRequestDto | MentorUpdateRequestDto;
const canAccessMentorFeatures = (user: { role?: { name?: string; permissions?: Array<{ name?: string }> } } | null) => {
if (!user?.role) return false;
const roleName = user.role.name?.toLowerCase() || '';
const isMentorRole = roleName.includes('mentor') || roleName.includes('admin');
if (isMentorRole) return true;
const permissions = user.role.permissions || [];
const hasMentorPermission = permissions.some((permission: { name?: string }) =>
permission.name?.toLowerCase().includes('mentor')
);
return hasMentorPermission;
};
interface ProfileContextType {
profileData: ProfileData | undefined;
isLoading: boolean;
error: unknown;
isOwnProfile: boolean;
profileId: string | null;
profileType: 'user' | 'mentor';
updateProfile: (data: ProfileUpdateData) => Promise<void>;
isUpdating: boolean;
canAccessMentor: boolean;
}const ProfileContext = createContext<ProfileContextType | undefined>(undefined);
interface ProfileProviderProps {
children: React.ReactNode;
profileId?: string;
profileType?: 'user' | 'mentor';
}
export const ProfileProvider: React.FC<ProfileProviderProps> = ({
children,
profileId,
profileType: forcedProfileType
}) => {
const params = useParams();
const { session } = useAuthStore();
const queryClient = useQueryClient();
const canAccessMentor = useMemo(() => {
return canAccessMentorFeatures(session?.user || null);
}, [session?.user]);
const isMentorRole = useMemo(() => {
const roleName = session?.user?.role?.name?.toLowerCase() || '';
return roleName === 'mentor';
}, [session?.user?.role?.name]);
const profileType: 'user' | 'mentor' = useMemo(() => {
if (forcedProfileType) {
if (forcedProfileType === 'mentor' && !isMentorRole) {
return 'user';
}
return forcedProfileType;
}
if ((params?.mentor || (typeof window !== 'undefined' && window.location.pathname.includes('/mentor'))) && isMentorRole) {
return 'mentor';
}
return 'user';
}, [forcedProfileType, params, isMentorRole]);
const id = profileId || (params?.id as string) || undefined;
const isOwnProfile = !id;
const userMeQuery = useUserMe({
queryKey: ['user-me'],
enabled: isOwnProfile && profileType === 'user',
});
const userByIdQuery = useUserById(id || '', {
queryKey: ['user-by-id', id],
enabled: !isOwnProfile && !!id && profileType === 'user',
});
const updateUserMeMutation = useUpdateUserMe();
const updateUserByIdMutation = useUpdateUserById();
const mentorMeQuery = useMentorMe({
queryKey: ['mentor-me'],
enabled: isOwnProfile && profileType === 'mentor' && canAccessMentor,
});
const mentorByIdQuery = useMentorById(id || '', {
queryKey: ['mentor-by-id', id],
enabled: !isOwnProfile && !!id && profileType === 'mentor' && canAccessMentor,
});
const updateMentorMeMutation = useUpdateMentorMe();
const updateMentorByIdMutation = useUpdateMentorById();
const selectedUserQuery = isOwnProfile ? userMeQuery : userByIdQuery;
const selectedMentorQuery = isOwnProfile ? mentorMeQuery : mentorByIdQuery;
const {
data: profileData,
isLoading,
error
} = useMemo(() => {
if (canAccessMentor && profileType === 'mentor') {
return selectedMentorQuery;
}
return selectedUserQuery;
}, [profileType, canAccessMentor, selectedUserQuery, selectedMentorQuery]);
const selectedUserMutation = isOwnProfile ? updateUserMeMutation : updateUserByIdMutation;
const selectedMentorMutation = isOwnProfile ? updateMentorMeMutation : updateMentorByIdMutation;
const updateMutation = useMemo(() => {
if (canAccessMentor && profileType === 'mentor') {
return selectedMentorMutation;
}
return selectedUserMutation;
}, [profileType, canAccessMentor, selectedUserMutation, selectedMentorMutation]);
const updateProfile = useCallback(async (data: ProfileUpdateData) => {
try {
if (canAccessMentor && profileType === 'mentor') {
if (isOwnProfile) {
await updateMentorMeMutation.mutateAsync(data as MentorUpdateRequestDto);
await queryClient.invalidateQueries({ queryKey: ['mentor-me'] });
} else if (id) {
await updateMentorByIdMutation.mutateAsync({ id, data: data as MentorUpdateRequestDto });
await queryClient.invalidateQueries({ queryKey: ['mentor-by-id', id] });
}
} else if (isOwnProfile) {
await updateUserMeMutation.mutateAsync(data as UserUpdateRequestDto);
await queryClient.invalidateQueries({ queryKey: ['user-me'] });
} else if (id) {
await updateUserByIdMutation.mutateAsync({ id, data: data as UserUpdateRequestDto });
await queryClient.invalidateQueries({ queryKey: ['user-by-id', id] });
}
} catch (error: unknown) {
console.error('Failed to update profile:', error);
let apiMessage = '';
if (typeof error === 'object' && error !== null) {
const errObj = error as { response?: { data?: unknown } };
const data = errObj.response?.data;
if (data) {
try {
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
if (parsed && typeof parsed.message === 'string') {
apiMessage = parsed.message;
}
} catch {
apiMessage = typeof data === 'string' ? data : '';
}
}
}
if (apiMessage) {
throw new Error(apiMessage);
}
throw error;
}
}, [
profileType,
isOwnProfile,
canAccessMentor,
id,
queryClient,
updateUserMeMutation,
updateUserByIdMutation,
updateMentorMeMutation,
updateMentorByIdMutation
]); const isUpdating = updateMutation.isPending;
const value: ProfileContextType = useMemo(() => ({
profileData,
isLoading,
error,
isOwnProfile,
profileId: isOwnProfile ? null : (id || null),
profileType,
updateProfile,
isUpdating,
canAccessMentor
}), [profileData, isLoading, error, isOwnProfile, id, profileType, updateProfile, isUpdating, canAccessMentor]);
return (
<ProfileContext.Provider value={value}>
{children}
</ProfileContext.Provider>
);
};
export const useProfile = (): ProfileContextType => {
const context = useContext(ProfileContext);
if (!context) {
throw new Error('useProfile must be used within a ProfileProvider');
}
return context;
};
export type { ProfileContextType };
export type { ProfileData, ProfileUpdateData };
@@ -1,27 +0,0 @@
'use client';
import React from 'react';
import { Guard } from '@imphnen-frontend-service/utils';
interface MentorGuardProps {
children: React.ReactNode;
fallback?: React.ReactNode;
}
export const MentorGuard: React.FC<MentorGuardProps> = ({
children,
fallback = (
<div className="p-4 text-center text-red-500">
<p>Akses ditolak: Anda tidak memiliki izin untuk mengakses fitur mentor.</p>
</div>
)
}) => {
return (
<Guard
permissions={['mentor', 'admin']}
fallback={fallback}
>
{children}
</Guard>
);
};
@@ -1,17 +0,0 @@
export * from './profile';
export * from './sections';
export * from './buttons';
export * from './shared';
export * from './modals';
export * from './contexts';
@@ -1,187 +0,0 @@
import { FC, useState, useEffect } from 'react';
import { ModalButton } from '../buttons/modal-button';
import { useUploadCV } from '@imphnen-frontend-service/service';
import { FileUploader } from '../shared/file-uploader';
interface CVData {
fileName: string;
fileUrl?: string;
}
interface CVModalProps {
isOpen: boolean;
onClose: () => void;
initialValue: CVData;
onSave: (value: CVData) => Promise<void>;
isLoading?: boolean;
}
export const CVModal: FC<CVModalProps> = ({
isOpen,
onClose,
initialValue,
onSave,
isLoading = false,
}) => {
const [cvData, setCvData] = useState(initialValue);
const [isUploading, setIsUploading] = useState(false);
const uploadCVMutation = useUploadCV();
useEffect(() => {
setCvData(initialValue);
}, [initialValue]);
const handleSave = async () => {
try {
await onSave(cvData);
onClose();
} catch (error) {
console.error('Save failed:', error);
}
};
const handleCancel = () => {
setCvData(initialValue);
onClose();
};
const handleFileSelect = async (file: File) => {
try {
setIsUploading(true);
if (!file.type.includes('pdf')) {
throw new Error('Please select a PDF file');
}
const uploadResult = await uploadCVMutation.mutateAsync(file);
console.log('CV upload response:', uploadResult);
interface UploadData {
original_filename?: string;
filename?: string;
url?: string;
}
const uploadData = ('data' in uploadResult ? (uploadResult as { data: UploadData }).data : uploadResult as UploadData);
setCvData({
fileName: uploadData.original_filename || uploadData.filename || file.name,
fileUrl: uploadData.url || '',
});
console.log('CV uploaded successfully, URL:', uploadData.url);
} catch (error) {
console.error('CV upload error:', error);
const fileInput = document.getElementById('cv-upload') as HTMLInputElement;
if (fileInput) fileInput.value = '';
} finally {
setIsUploading(false);
}
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<button
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
onClick={handleCancel}
aria-label="Close modal"
type="button"
/>
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-5xl mx-auto">
<div className="p-6 pb-4 border-b border-gray-200">
<div className="flex">
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit CV/Resume</h2>
</div>
</div>
<div className="px-6 py-6 space-y-6">
{}
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-700 mb-2">
Upload CV/Resume
</h3>
<FileUploader
accept=".pdf"
maxSize={10 * 1024 * 1024}
onFileSelect={handleFileSelect}
isLoading={isUploading}
dragAndDrop={true}
description="Klik atau tarik file PDF yang ingin di upload"
className="w-full"
/>
<p className="text-xs text-gray-500 mt-2">
Format yang didukung: PDF • Maksimal ukuran: 10MB
</p>
</div>
{}
{cvData.fileName && (
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="text-sm font-medium text-blue-900 mb-3">File Terpilih</h4>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center">
<span className="text-white text-xs font-bold">PDF</span>
</div>
<div className="flex-1">
<p className="font-medium text-blue-900 text-sm">{cvData.fileName}</p>
<p className="text-xs text-blue-600">Siap untuk disimpan</p>
</div>
{cvData.fileUrl && (
<a
href={cvData.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 text-sm font-medium underline"
>
Preview
</a>
)}
</div>
</div>
)}
</div>
</div>
{}
<div className="flex gap-3 p-6 pt-4 border-t border-gray-100">
<ModalButton
variant="secondary"
onClick={handleCancel}
className="flex-1"
disabled={isLoading || isUploading}
>
Batal
</ModalButton>
<ModalButton
variant="primary"
onClick={handleSave}
className="flex-1"
disabled={isLoading || isUploading || !cvData.fileName}
loading={isLoading}
>
{isLoading ? 'Menyimpan...' : 'Simpan CV'}
</ModalButton>
</div>
</div>
</div>
);
};
@@ -1,99 +0,0 @@
import { FC, useState, useEffect } from 'react';
import { ModalButton } from '../buttons/modal-button';
interface DescriptionModalProps {
isOpen: boolean;
onClose: () => void;
initialValue: string;
onSave: (value: string) => Promise<void>;
isLoading?: boolean;
}
export const DescriptionModal: FC<DescriptionModalProps> = ({
isOpen,
onClose,
initialValue,
onSave,
isLoading = false,
}) => {
const [description, setDescription] = useState(initialValue);
useEffect(() => {
setDescription(initialValue);
}, [initialValue]);
const handleSave = async () => {
try {
await onSave(description);
onClose();
} catch (error) {
console.error('Save failed:', error);
}
};
const handleCancel = () => {
setDescription(initialValue);
onClose();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<button
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
onClick={handleCancel}
aria-label="Close modal"
type="button"
/>
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-5xl mx-auto">
<div className="p-6 pb-4 border-b border-gray-200">
<div className="flex">
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit Description</h2>
</div>
</div>
<div className="px-6 py-4">
<div>
<label htmlFor="description-textarea" className="block text-sm font-medium text-gray-700 mb-2">
Description
</label>
<textarea
id="description-textarea"
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={8}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#23A1EB] focus:border-[#23A1EB] outline-none transition-colors resize-none"
placeholder="Write your description here..."
/>
</div>
</div>
<div className="flex gap-3 p-6 pt-4">
<ModalButton
variant="secondary"
onClick={handleCancel}
className="flex-1"
disabled={isLoading}
>
Batal
</ModalButton>
<ModalButton
variant="primary"
onClick={handleSave}
className="flex-1"
disabled={isLoading}
loading={isLoading}
>
{isLoading ? 'Menyimpan...' : 'Simpan'}
</ModalButton>
</div>
</div>
</div>
);
};
@@ -1,253 +0,0 @@
import React, { FC, useState, useEffect } from 'react';
import { Modal, InputField } from '@imphnen-frontend-service/ui/molecules';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { useProfile } from '../contexts/profile-context';
import { CameraOutlined } from '@ant-design/icons';
import { useUploadAvatar } from '@imphnen-frontend-service/service';
interface EditProfileModalProps {
isOpen: boolean;
onClose: () => void;
showNotification: (type: 'success' | 'error', title: string, message?: string) => void;
}
export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, showNotification }) => {
const { profileData, profileType, updateProfile } = useProfile();
const uploadAvatarMutation = useUploadAvatar();
const [formData, setFormData] = useState({
fullname: '',
avatar: ''
});
const [previewUrl, setPreviewUrl] = useState<string>('/image/testimonial.webp');
const [isUploading, setIsUploading] = useState(false);
useEffect(() => {
if (profileData) {
const fullname = profileData.fullname ||
(profileType === 'mentor' && 'legal_name' in profileData ? profileData.legal_name : '') || '';
const avatar = (profileType === 'user' && 'avatar' in profileData)
? profileData.avatar || '/image/testimonial.webp'
: '/image/testimonial.webp';
console.log('Modal - Profile data avatar URL:', avatar);
console.log('Modal - Profile data:', profileData);
setFormData({
fullname,
avatar: (profileType === 'user' && 'avatar' in profileData) ? profileData.avatar || '' : ''
});
setPreviewUrl(avatar);
} else {
setPreviewUrl('/image/testimonial.webp');
}
}, [profileData, profileType]);
const handleImageError = () => {
console.log('Image failed to load:', previewUrl);
setPreviewUrl('/image/testimonial.webp');
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setFormData(prev => ({ ...prev, fullname: e.target.value }));
};
const handleImageUpload = async (file: File) => {
try {
setIsUploading(true);
const reader = new FileReader();
reader.onload = () => {
const result = reader.result as string;
setPreviewUrl(result);
};
reader.readAsDataURL(file);
const uploadResult = await uploadAvatarMutation.mutateAsync(file);
console.log('Avatar upload response:', uploadResult);
interface UploadData {
url?: string;
}
const uploadData = ('data' in uploadResult ? (uploadResult as { data: UploadData }).data : uploadResult as UploadData);
setFormData(prev => ({ ...prev, avatar: uploadData.url || '' }));
setPreviewUrl(uploadData.url || '/image/testimonial.webp');
} catch (error) {
console.error('Avatar upload error:', error);
showNotification('error', 'Upload Failed', 'Failed to upload avatar image');
const originalAvatar = (profileType === 'user' && profileData && 'avatar' in profileData) ? profileData.avatar : '';
setPreviewUrl(originalAvatar || '/image/testimonial.webp');
} finally {
setIsUploading(false);
}
};
const handleSave = async () => {
try {
const updates: Record<string, string> = {};
if (formData.fullname.trim() !== '') {
if (profileType === 'user') {
updates.fullname = formData.fullname;
} else if (profileType === 'mentor') {
updates.legal_name = formData.fullname;
}
}
if (formData.avatar && formData.avatar !== (profileData && 'avatar' in profileData ? profileData.avatar : '')) {
updates.avatar = formData.avatar;
}
if (Object.keys(updates).length > 0) {
await updateProfile(updates);
showNotification('success', 'Profile Updated', 'Your profile has been successfully updated.');
}
onClose();
} catch (err: unknown) {
console.error('Profile update error:', err);
let apiMessage = '';
if (typeof err === 'object' && err !== null) {
const errObj = err as { response?: { data?: { message?: string } } };
let backendMsg = '';
if (errObj.response?.data?.message) {
backendMsg = errObj.response.data.message;
}
let msg = '';
if ('message' in err && typeof (err as { message?: string }).message === 'string') {
msg = (err as { message?: string }).message || '';
if (msg.trim().startsWith('{') && msg.trim().endsWith('}')) {
try {
const parsed = JSON.parse(msg);
if (parsed && typeof parsed.message === 'string') {
msg = parsed.message;
}
} catch {
// Ignore JSON parse errors
}
}
}
if (backendMsg && msg && backendMsg !== msg) {
apiMessage = backendMsg + '\n' + msg;
} else if (backendMsg) {
apiMessage = backendMsg;
} else if (msg) {
apiMessage = msg;
}
}
showNotification('error', 'Failed to save changes', apiMessage || 'Please try again.');
}
}; return (
<Modal isOpen={isOpen} onClose={onClose}>
<Modal.Header>
<Modal.Title>Edit Profile</Modal.Title>
</Modal.Header>
<Modal.Content>
{}
<div className="flex justify-center pt-6 pb-4">
<div className="relative">
{}
<input
id="avatar-upload"
type="file"
accept="image/*"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) handleImageUpload(file);
}}
className="hidden"
disabled={isUploading}
/>
<label htmlFor="avatar-upload" className="cursor-pointer block relative group">
<img
src={previewUrl}
alt="Profile"
className="w-24 h-24 rounded-full object-cover border-4 border-blue-100 shadow-lg transition-all duration-300 group-hover:border-blue-200"
onError={handleImageError}
/>
{/* Hover overlay */}
<div className="absolute inset-0 rounded-full bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 flex items-center justify-center">
<span className="text-white text-xs opacity-0 group-hover:opacity-100 transition-opacity duration-300">
Change Photo
</span>
</div>
</label>
{}
<label htmlFor="avatar-upload" className="cursor-pointer">
<div className="absolute bottom-0 right-0 w-8 h-8 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-300 shadow-lg border-2 border-white">
{isUploading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : (
<CameraOutlined className="text-xs" />
)}
</div>
</label>
</div>
</div> <div className="px-6 pb-6 space-y-6">
{}
<div className="space-y-2">
<InputField
label="Full Name"
name="fullname"
value={formData.fullname}
onChange={handleNameChange}
placeholder="Enter your full name"
className="w-full"
/>
<p className="text-xs text-gray-500 pl-1">
This name will be displayed on your profile
</p>
</div>
</div>
</Modal.Content>
<Modal.Footer>
<div className="flex gap-3 w-full">
<Button
variant="secondary"
onClick={onClose}
className="flex-1"
disabled={isUploading}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSave}
className="flex-1"
disabled={isUploading}
>
{isUploading ? (
<>
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
Saving...
</>
) : (
'Save Changes'
)}
</Button>
</div>
</Modal.Footer>
</Modal>
);
};
Loaded 50 of 226 files, more files were not shown because too many files have changed in this diff. Show more