diff --git a/package.json b/package.json index 44dae91..28c53b7 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-select": "^2.1.4", "@radix-ui/react-slot": "^1.1.1", + "@refinedev/core": "^4.57.5", + "@refinedev/simple-rest": "^5.0.10", "@tanstack/react-query": "^5.65.1", "axios": "^1.7.9", "class-variance-authority": "^0.7.1", diff --git a/src/app/(refine)/layout.tsx b/src/app/(refine)/layout.tsx new file mode 100644 index 0000000..7dd3ec2 --- /dev/null +++ b/src/app/(refine)/layout.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { Refine } from '@refinedev/core'; +import dataProvider from '@refinedev/simple-rest'; +import axios from 'axios'; +import { config } from '@/app/config'; + +// Create axios instance with auth headers +const axiosInstance = axios.create({ + baseURL: config.baseURL + '/api', + headers: { + 'Content-Type': 'application/json', + }, +}); + +axiosInstance.interceptors.request.use((config) => { + const token = + typeof window !== 'undefined' ? localStorage.getItem('auth-data') : null; + if (token) { + const parsedToken = JSON.parse(token); + config.headers.Authorization = `Bearer ${parsedToken.accessToken}`; + } + return config; +}); + +// Create authenticated data provider +const customDataProvider = dataProvider(config.baseURL + '/api', axiosInstance); + + +export default function Layout({ children }: React.PropsWithChildren) { + return ( + + {children} + + ); +} \ No newline at end of file diff --git a/src/app/(refine)/refine/_components/checkInList.tsx b/src/app/(refine)/refine/_components/checkInList.tsx new file mode 100644 index 0000000..aea5875 --- /dev/null +++ b/src/app/(refine)/refine/_components/checkInList.tsx @@ -0,0 +1,70 @@ +// components/CheckInList.tsx +'use client'; +import { ChevronLeft, ChevronRight } from 'lucide-react'; +import dayjs from 'dayjs'; +import { User } from '../libs/interface'; + +export const CheckInList = ({ + checkIns, + page, + pageSize, + setPage, +}: { + checkIns: User[]; + page: number; + pageSize: number; + setPage: (page: number) => void; +}) => { + const totalPages = Math.ceil(checkIns.length / pageSize); + const paginatedCheckIns = checkIns.slice((page - 1) * pageSize, page * pageSize); + + return ( +
+
+ + เช็คอินวันนี้: {checkIns.length} คน + +
+ + + + {checkIns.length > pageSize && ( +
+
+
+ หน้า {page} จาก {totalPages} +
+
+ + +
+
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/app/(refine)/refine/_components/doughnutChart.tsx b/src/app/(refine)/refine/_components/doughnutChart.tsx new file mode 100644 index 0000000..b0f6314 --- /dev/null +++ b/src/app/(refine)/refine/_components/doughnutChart.tsx @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// components/DoughnutChart.tsx +'use client'; +import { Doughnut } from 'react-chartjs-2'; +import { ChartOptions } from 'chart.js'; + +export const DoughnutChart = ({ + data, + options, +}: { + data: any; + options: ChartOptions<'doughnut'>; +}) => { + return ; +}; \ No newline at end of file diff --git a/src/app/(refine)/refine/_components/header.tsx b/src/app/(refine)/refine/_components/header.tsx new file mode 100644 index 0000000..4cbc2f7 --- /dev/null +++ b/src/app/(refine)/refine/_components/header.tsx @@ -0,0 +1,35 @@ +// components/DashboardHeader.tsx +'use client'; +import Link from 'next/link'; +import { ChevronLeft, Users } from 'lucide-react'; + +export const DashboardHeader = ({ + total, + children, +}: { + total: number; + children: React.ReactNode; +}) => ( +
+
+
+ + +

+ แดชบอร์ดลงทะเบียน +

+ +
+ + + ผู้ลงทะเบียนทั้งหมด: {total} คน + +
+
+
{children}
+
+
+); \ No newline at end of file diff --git a/src/app/(refine)/refine/_components/lineChart.tsx b/src/app/(refine)/refine/_components/lineChart.tsx new file mode 100644 index 0000000..3da6933 --- /dev/null +++ b/src/app/(refine)/refine/_components/lineChart.tsx @@ -0,0 +1,15 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// components/LineChart.tsx +'use client'; +import { Line } from 'react-chartjs-2'; +import { ChartOptions } from 'chart.js'; + +export const LineChart = ({ + data, + options, +}: { + data: any; + options: ChartOptions<'line'>; +}) => { + return ; +}; \ No newline at end of file diff --git a/src/app/(refine)/refine/dashboard/_components/exportButton.tsx b/src/app/(refine)/refine/dashboard/_components/exportButton.tsx new file mode 100644 index 0000000..a44eb53 --- /dev/null +++ b/src/app/(refine)/refine/dashboard/_components/exportButton.tsx @@ -0,0 +1,74 @@ +// components/ExportButton.tsx +'use client'; +import { Download } from 'lucide-react'; +import { FieldEntry, User } from '../../libs/interface'; +import dayjs from 'dayjs'; + +export const ExportButton = ({ data }: { data: User[] }) => { + // Define fields to export (all User fields except imageUrl) + const fieldEntries: FieldEntry[] = [ + { label: 'ID', field: 'id' }, + { label: 'UID', field: 'uid' }, + { label: 'Name', field: 'name' }, + { label: 'Email', field: 'email' }, + { label: 'Phone', field: 'phone' }, + { label: 'University', field: 'university' }, + { label: 'Size Jersey', field: 'sizeJersey' }, + { label: 'Food Limitation', field: 'foodLimitation' }, + { label: 'Invitation Code', field: 'invitationCode' }, + { label: 'Age', field: 'age' }, + { label: 'Chronic Disease', field: 'chronicDisease' }, + { label: 'Drug Allergy', field: 'drugAllergy' }, + { label: 'Status', field: 'status' }, + { label: 'Graduated Year', field: 'graduatedYear' }, + { label: 'Faculty', field: 'faculty' }, + { label: 'Last Entered', field: 'lastEntered' }, + { label: 'Registered At', field: 'registeredAt' }, + { label: 'Role', field: 'role' }, + { label: 'Education', field: 'education' }, + { label: 'Is Acrophobic', field: 'isAcrophobic' }, + ]; + + const handleExportCSV = () => { + const headers = fieldEntries.map(f => f.label).join(','); + + const csvContent = [ + headers, + ...data.map(user => + fieldEntries.map(({ field }) => { + const value = user[field]; + // Handle date fields + if (field === 'lastEntered' || field === 'registeredAt') { + return value && dayjs(value.toString()).isValid() + ? dayjs(value.toString()).format('YYYY-MM-DD HH:mm') + : ''; + } + // Escape quotes in string fields + return typeof value === 'string' + ? `"${value.replace(/"/g, '""')}"` + : value; + }).join(',') + ) + ].join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `users_export_${dayjs().format('YYYYMMDD_HHmmss')}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + }; + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/(refine)/refine/dashboard/_components/pagination.tsx b/src/app/(refine)/refine/dashboard/_components/pagination.tsx new file mode 100644 index 0000000..784299c --- /dev/null +++ b/src/app/(refine)/refine/dashboard/_components/pagination.tsx @@ -0,0 +1,126 @@ +import { ChevronLeft, ChevronRight } from 'lucide-react'; + +export const Pagination = ({ + current, + totalPages, + pageSize, + totalItems, + setCurrent, + setPageSize, +}: { + current: number; + totalPages: number; + pageSize: number; + totalItems: number; + setCurrent: (page: number) => void; + setPageSize: (size: number) => void; +}) => { + const hasNext = current < totalPages; + const hasPrev = current > 1; + + return ( +
+
+ แสดง {(current - 1) * pageSize + 1} -{' '} + {Math.min(current * pageSize, totalItems)} จาก {totalItems} +
+ +
+ +
+ + +
+ ); +}; + +const PaginationControls = ({ + current, + totalPages, + hasPrev, + hasNext, + setCurrent, +}: { + current: number; + totalPages: number; + hasPrev: boolean; + hasNext: boolean; + setCurrent: (page: number) => void; +}) => ( + <> + + + +
+ {Array.from({ length: totalPages }, (_, i) => ( + + ))} +
+ + + + +); + +const PageSizeSelect = ({ + pageSize, + setPageSize, +}: { + pageSize: number; + setPageSize: (size: number) => void; +}) => ( +
+ แสดงต่อหน้า: + +
+); \ No newline at end of file diff --git a/src/app/(refine)/refine/dashboard/_components/searchBar.tsx b/src/app/(refine)/refine/dashboard/_components/searchBar.tsx new file mode 100644 index 0000000..5ba702b --- /dev/null +++ b/src/app/(refine)/refine/dashboard/_components/searchBar.tsx @@ -0,0 +1,34 @@ +import { Search } from 'lucide-react'; + +export const SearchBar = ({ + searchTerm, + searchBy, + setSearchTerm, + setSearchBy, +}: { + searchTerm: string; + searchBy: 'name' | 'uid'; + setSearchTerm: (value: string) => void; + setSearchBy: (value: 'name' | 'uid') => void; +}) => ( +
+ +
+ setSearchTerm(e.target.value)} + /> + +
+
+); \ No newline at end of file diff --git a/src/app/(refine)/refine/dashboard/_components/userTable.tsx b/src/app/(refine)/refine/dashboard/_components/userTable.tsx new file mode 100644 index 0000000..b034881 --- /dev/null +++ b/src/app/(refine)/refine/dashboard/_components/userTable.tsx @@ -0,0 +1,20 @@ +export const UserTable = ({ children }: { children: React.ReactNode }) => ( +
+ + + + + + + + + + + + + + {children} + +
UIDชื่ออีเมลสถานะคณะขนาดเสื้อเข้างานล่าสุด
+
+ ); \ No newline at end of file diff --git a/src/app/(refine)/refine/dashboard/_components/userTableRow.tsx b/src/app/(refine)/refine/dashboard/_components/userTableRow.tsx new file mode 100644 index 0000000..09439c7 --- /dev/null +++ b/src/app/(refine)/refine/dashboard/_components/userTableRow.tsx @@ -0,0 +1,49 @@ +import dayjs from 'dayjs'; +import { User } from '../../libs/interface'; + +export const UserTableRow = ({ user, onClick }: { user: User, onClick: () => void }) => { + const statusStyles = { + chula_student: 'bg-blue-600', + alumni: 'bg-pink-600', + general_student: 'bg-gray-600', + }; + const style = statusStyles[user.status as keyof typeof statusStyles]; + + return ( + + + {user.uid} + + + {user.name} + + + {user.email} + + + + { + { + chula_student: 'นิสิตปัจจุบัน', + alumni: 'ศิษย์เก่า', + general_student: 'บุคคลทั่วไป', + }[user.status] + } + + + + {user.faculty} + + + {user.sizeJersey} + + + {dayjs(user.lastEntered).isValid() + ? dayjs(user.lastEntered).format('DD/MM HH:mm') + : 'ยังไม่เข้างาน'} + + + ); +}; diff --git a/src/app/(refine)/refine/dashboard/page.tsx b/src/app/(refine)/refine/dashboard/page.tsx new file mode 100644 index 0000000..b2203e9 --- /dev/null +++ b/src/app/(refine)/refine/dashboard/page.tsx @@ -0,0 +1,94 @@ +'use client'; +import { useTable } from '@refinedev/core'; +import { ChevronLeft } from 'lucide-react'; +import { useState, useMemo } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { User } from '../libs/interface'; +import { SearchBar } from './_components/searchBar'; +import { UserTable } from './_components/userTable'; +import { UserTableRow } from './_components/userTableRow'; +import { Pagination } from './_components/pagination'; +import { ExportButton } from './_components/exportButton'; // Add this import + +const DataTable = () => { + const router = useRouter(); + const [searchTerm, setSearchTerm] = useState(''); + const [searchBy, setSearchBy] = useState<'name' | 'uid'>('name'); + const PAGE_SIZE = 15; + + const { tableQuery, current, setCurrent, pageSize, setPageSize } = + useTable({ + resource: 'users', + pagination: { + mode: 'server', + current: 1, + pageSize: PAGE_SIZE, + }, + }); + + const filteredData = useMemo(() => { + if (!tableQuery.data?.data) return []; + return tableQuery.data.data.filter(user => + user[searchBy].toLowerCase().includes(searchTerm.toLowerCase()), + ); + }, [tableQuery.data, searchTerm, searchBy]); + + const paginatedData = useMemo(() => { + const start = (current - 1) * pageSize; + const end = start + pageSize; + return filteredData.slice(start, end); + }, [filteredData, current, pageSize]); + + const totalItems = filteredData.length; + const totalPages = Math.ceil(totalItems / pageSize); + + return ( +
+
+
+ + +

+ ข้อมูลผู้ลงทะเบียนทั้งหมด +

+ + +
+ + {/* Add ExportButton here */} +
+
+ + + {paginatedData.map(user => ( + router.push(`/refine/users/${user.id}`)} + /> + ))} + + + +
+
+ ); +}; + +export default DataTable; \ No newline at end of file diff --git a/src/app/(refine)/refine/libs/interface.ts b/src/app/(refine)/refine/libs/interface.ts new file mode 100644 index 0000000..c022c02 --- /dev/null +++ b/src/app/(refine)/refine/libs/interface.ts @@ -0,0 +1,28 @@ +export interface User { + id: string; + uid: string; + name: string; + email: string; + phone: string; + university: string; + sizeJersey: string; + foodLimitation: string; + invitationCode: string; + age: string; + chronicDisease: string; + drugAllergy: string; + status: string; + graduatedYear: string; + faculty: string; + imageUrl: string; + lastEntered: string; + registeredAt: string; + role: string; + education: string; + isAcrophobic: boolean; +} + +export type FieldEntry = { + label: string; + field: keyof User; +}; diff --git a/src/app/(refine)/refine/libs/stat.ts b/src/app/(refine)/refine/libs/stat.ts new file mode 100644 index 0000000..5b71ece --- /dev/null +++ b/src/app/(refine)/refine/libs/stat.ts @@ -0,0 +1,48 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// lib/stats.ts +import dayjs from 'dayjs'; +import { User } from '../libs/interface'; + +export const processStats = (data: User[]) => { + const today = dayjs().format('YYYY-MM-DD'); + + const statusStatistics = { + currentStudent: data.filter(u => u.status === 'chula_student').length, + alumni: data.filter(u => u.status === 'alumni').length, + generalPublic: data.filter(u => u.status === 'general_student').length, + otherStudents: data.filter( + u => !['chula_student', 'alumni', 'general_student'].includes(u.status), + ).length, + }; + + const dailyRegistrations = data.reduce((acc, user) => { + const date = dayjs(user.registeredAt).format('YYYY-MM-DD'); + if (date) acc[date] = (acc[date] || 0) + 1; + return acc; + }, {} as Record); + + const todaysCheckIns = data.filter( + user => dayjs(user.lastEntered).format('YYYY-MM-DD') === today, + ); + + return { statusStatistics, dailyRegistrations, todaysCheckIns }; +}; + +export const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom' as const, + labels: { + font: { size: 14 }, + usePointStyle: true, + }, + }, + tooltip: { + callbacks: { + label: (context: any) => `${context.dataset.label}: ${context.raw} คน`, + }, + }, + }, +}; \ No newline at end of file diff --git a/src/app/(refine)/refine/page.tsx b/src/app/(refine)/refine/page.tsx new file mode 100644 index 0000000..7332889 --- /dev/null +++ b/src/app/(refine)/refine/page.tsx @@ -0,0 +1,165 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// app/dashboard/page.tsx +'use client'; +import { useTable } from '@refinedev/core'; +import { useState } from 'react'; +import Link from 'next/link'; +import dayjs from 'dayjs'; +import { User } from './libs/interface'; +import { DashboardHeader } from '../refine/_components/header'; +import { LineChart } from '../refine/_components/lineChart'; +import { DoughnutChart } from '../refine/_components/doughnutChart'; +import { CheckInList } from '../refine/_components/checkInList'; +import { processStats, chartOptions } from '../refine/libs/stat'; +import { ScanLine, UserCheck, Users } from 'lucide-react'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + ArcElement, +} from 'chart.js'; + +const CHECK_IN_PAGE_SIZE = 5; + +// Register Chart.js components +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, + ArcElement +); + +const Dashboard = () => { + const [checkInPage, setCheckInPage] = useState(1); + const { tableQuery } = useTable({ resource: 'users' }); + const data = tableQuery?.data?.data || []; + const total = tableQuery?.data?.total || 0; + + const { statusStatistics, dailyRegistrations, todaysCheckIns } = processStats(data as User[]); + + // Chart data preparations + const doughnutData = { + labels: ['นิสิตปัจจุบัน', 'ศิษย์เก่า', 'บุคคลทั่วไป', 'นักศึกษาอื่น'], + datasets: [ + { + label: 'จำนวนผู้ลงทะเบียน', + data: Object.values(statusStatistics), + backgroundColor: ['#708FCE', '#DF729F', '#9BCDEB', '#757575'], + borderWidth: 0, + }, + ], + }; + + const lineData = { + labels: Object.keys(dailyRegistrations) + .sort() + .map(date => dayjs(date).format('DD/MM')), + datasets: [ + { + label: 'ผู้ลงทะเบียน', + data: Object.values(dailyRegistrations), + borderColor: '#DF729F', + backgroundColor: '#E9B0CC', + tension: 0.4, + fill: true, + }, + ], + }; + + return ( +
+ + + ดูข้อมูลทั้งหมด + + + +
+
+
+ +

+ แนวโน้มการลงทะเบียนรายวัน +

+
+
+ `${value} คน`, + stepSize: 1, + color: '#6b7280', + }, + grid: { color: '#f3f4f6' }, + }, + }, + }} + /> +
+
+ +
+
+ +

สถิติตามสถานะ

+
+
+ +
+
+
+ +
+
+ +

สถานการณ์วันนี้

+
+ +
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/src/app/(refine)/refine/users/[id]/_components/infoRow.tsx b/src/app/(refine)/refine/users/[id]/_components/infoRow.tsx new file mode 100644 index 0000000..f903013 --- /dev/null +++ b/src/app/(refine)/refine/users/[id]/_components/infoRow.tsx @@ -0,0 +1,18 @@ +export const InfoRow = ({ + label, + value, + onEdit, +}: { + label: string; + value: string; + onEdit?: () => void; +}) => ( +
+ {label} + {value} +
+); \ No newline at end of file diff --git a/src/app/(refine)/refine/users/[id]/_components/userDetails.tsx b/src/app/(refine)/refine/users/[id]/_components/userDetails.tsx new file mode 100644 index 0000000..5e7c53d --- /dev/null +++ b/src/app/(refine)/refine/users/[id]/_components/userDetails.tsx @@ -0,0 +1,233 @@ +/* eslint-disable @next/next/no-img-element */ +'use client'; +import { useOne } from '@refinedev/core'; +import { User, FieldEntry } from '../../../libs/interface'; +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; + +const formatThaiDate = (dateString: string) => { + const date = new Date(dateString); + date.setFullYear(date.getFullYear()); + return date.toLocaleDateString('th-TH', { + day: 'numeric', + month: 'long', + year: 'numeric', + }); +}; + +export default function UserDetails({ id }: { id: string }) { + const router = useRouter(); + const [isImageOpen, setIsImageOpen] = useState(false); + const [image, setImage] = useState(null); + const [imageError, setImageError] = useState(''); + const { data, isError, isLoading } = useOne({ + resource: 'users', + id: id, + }); + + const { data: imageData, isError: isImageError } = useOne({ + resource: 'users/image', + id: id, + }); + + const user = data?.data; + + useEffect(() => { + if (isImageError) { + setImageError('คุณไม่มีสิทธิ์เข้าถึงภาพนี้'); + setImage(null); + } else if (imageData?.data?.url) { + setImage(imageData.data.url); + setImageError(''); + } + }, [imageData, isImageError]); + + const createField = (label: string, field: keyof User): FieldEntry => ({ + label, + field, + }); + + const renderSection = (title: string, fields: FieldEntry[]) => ( +
+

{title}

+
+ {fields.map(({ label, field }) => { + const value = user?.[field]; + let displayValue = value || 'N/A'; + + if (field === 'registeredAt') { + displayValue = formatThaiDate(value as string); + } else if (field === 'graduatedYear') { + displayValue = (parseInt(value as string) + 543).toString(); + } else if (field === 'status') { + displayValue = value as string; + } else if (field === 'isAcrophobic') { + displayValue = value ? 'ใช่' : 'ไม่'; + } + + return ( +
+ {label} + {displayValue} +
+ ); + })} +
+
+ ); + + if (isLoading) return
กำลังโหลด...
; + if (isError) + return ( +
+ เกิดข้อผิดพลาดในการโหลดข้อมูลผู้ใช้ +
+ ); + if (!user) return
ไม่พบผู้ใช้
; + + return ( +
+ {/* Header */} +
+ +
+ + {/* Image Preview */} +
+
setIsImageOpen(true)} + className="relative aspect-[4/3] w-full overflow-hidden rounded-2xl bg-gray-100 shadow-lg" + > + {imageError ? ( +
+ + + + {imageError} +
+ ) : ( + image && ( + Citizen Card Preview + ) + )} +
+
+ + {/* Profile Information */} +
+
+

{user.name}

+
+ + {user.role} + + {user.uid} +
+
+ +
+ {renderSection('ข้อมูลส่วนตัว', [ + createField('อีเมล', 'email'), + createField('เบอร์โทรศัพท์', 'phone'), + createField('สถานะ', 'status'), + createField('วันที่ลงทะเบียน', 'registeredAt'), + ])} + + {renderSection('ข้อมูลการศึกษา', [ + createField('มหาวิทยาลัย', 'university'), + createField('คณะ', 'faculty'), + createField('ระดับการศึกษา', 'education'), + createField('ปีที่จบการศึกษา', 'graduatedYear'), + ])} + + {renderSection('ข้อมูลเพิ่มเติม', [ + createField('ขนาดเสื้อ', 'sizeJersey'), + createField('ข้อจำกัดด้านอาหาร', 'foodLimitation'), + createField('กลัวความสูงไหม', 'isAcrophobic'), + ...(user.chronicDisease + ? [createField('โรคประจำตัว', 'chronicDisease')] + : []), + ...(user.drugAllergy + ? [createField('อาการแพ้ยา', 'drugAllergy')] + : []), + ])} +
+
+ + {/* Image Modal */} + {isImageOpen && ( +
setIsImageOpen(false)} + > +
+ +
+ {image ? ( + Citizen Card + ) : ( +
+ {imageError} +
+ )} +
+
+
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/app/(refine)/refine/users/[id]/page.tsx b/src/app/(refine)/refine/users/[id]/page.tsx new file mode 100644 index 0000000..936cb8e --- /dev/null +++ b/src/app/(refine)/refine/users/[id]/page.tsx @@ -0,0 +1,9 @@ +// This is the server component that handles routing +import UserDetails from './_components/userDetails'; + +type tParams = Promise<{ id: string }>; + +export default async function Page(props: { params: tParams }) { + const { id } = await props.params; + return ; +} diff --git a/src/app/staff/qr/page.tsx b/src/app/staff/qr/page.tsx index dba0abd..78e93bb 100644 --- a/src/app/staff/qr/page.tsx +++ b/src/app/staff/qr/page.tsx @@ -3,6 +3,7 @@ import TopPart from '@/components/staff/qr/toppart'; import Edit from '@/components/staff/qr/editbutton'; import QrButton from '@/components/staff/qr/qrbutton'; import Protect from '@/components/protect'; +import Refine from '@/components/staff/qr/refineButton'; export default function Home() { return (
@@ -12,6 +13,7 @@ export default function Home() { +
diff --git a/src/components/staff/qr/refineButton.tsx b/src/components/staff/qr/refineButton.tsx new file mode 100644 index 0000000..d013616 --- /dev/null +++ b/src/components/staff/qr/refineButton.tsx @@ -0,0 +1,22 @@ +import { getImageURL } from '@/utils/image'; +import Image from 'next/image'; +import Link from 'next/link'; + +export default function Refine() { + return ( + +
+ edit +

แดชบอร์ด

+
+ + ); +}