Commit fef8775c authored by jorke's avatar jorke

Initial commit

parent 0aef20eb
Pipeline #52298 failed with stages
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development', // opsional
});
/** @type {import('next').NextConfig} */
const nextConfig = {
// konfigurasi lain jika ada
async rewrites() {
return [
{
source: '/api/absen/:path*', // FE route
destination: 'https://salma.kpk.go.id/api/absen/:path*', // BE target
},
];
},
};
module.exports = withPWA(nextConfig);
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
This diff is collapsed.
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
{
"name": "Absensi PWA",
"short_name": "Absensi",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#0f172a",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
[
{
"weights":
[
{"name":"conv0/filters","shape":[3,3,3,16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.009007044399485869,"min":-1.2069439495311063}},
{"name":"conv0/bias","shape":[16],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.005263455241334205,"min":-0.9211046672334858}},
{"name":"conv1/depthwise_filter","shape":[3,3,16,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.004001977630690033,"min":-0.5042491814669441}},
{"name":"conv1/pointwise_filter","shape":[1,1,16,32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.013836609615999109,"min":-1.411334180831909}},
{"name":"conv1/bias","shape":[32],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0015159862590771096,"min":-0.30926119685173037}},
{"name":"conv2/depthwise_filter","shape":[3,3,32,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002666276225856706,"min":-0.317286870876948}},
{"name":"conv2/pointwise_filter","shape":[1,1,32,64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.015265831292844286,"min":-1.6792414422128714}},
{"name":"conv2/bias","shape":[64],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0020280554598453,"min":-0.37113414915168985}},
{"name":"conv3/depthwise_filter","shape":[3,3,64,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006100742489683862,"min":-0.8907084034938438}},
{"name":"conv3/pointwise_filter","shape":[1,1,64,128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.016276211832083907,"min":-2.0508026908425725}},
{"name":"conv3/bias","shape":[128],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.003394414279975143,"min":-0.7637432129944072}},
{"name":"conv4/depthwise_filter","shape":[3,3,128,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.006716050119961009,"min":-0.8059260143953211}},
{"name":"conv4/pointwise_filter","shape":[1,1,128,256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.021875603993733724,"min":-2.8875797271728514}},
{"name":"conv4/bias","shape":[256],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.0041141652009066415,"min":-0.8187188749804216}},
{"name":"conv5/depthwise_filter","shape":[3,3,256,1],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008423839597141042,"min":-0.9013508368940915}},
{"name":"conv5/pointwise_filter","shape":[1,1,256,512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.030007277283014035,"min":-3.8709387695088107}},
{"name":"conv5/bias","shape":[512],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.008402082966823203,"min":-1.4871686851277068}},
{"name":"conv8/filters","shape":[1,1,512,25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.028336129469030042,"min":-4.675461362389957}},
{"name":"conv8/bias","shape":[25],"dtype":"float32","quantization":{"dtype":"uint8","scale":0.002268134028303857,"min":-0.41053225912299807}}
],
"paths":
[
"tiny_face_detector_model.bin"
]
}
]
\ No newline at end of file
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didn’t register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-e43f5367'], (function (workbox) { 'use strict';
importScripts();
self.skipWaiting();
workbox.clientsClaim();
workbox.registerRoute("/", new workbox.NetworkFirst({
"cacheName": "start-url",
plugins: [{
cacheWillUpdate: async ({
request,
response,
event,
state
}) => {
if (response && response.type === 'opaqueredirect') {
return new Response(response.body, {
status: 200,
statusText: 'OK',
headers: response.headers
});
}
return response;
}
}]
}), 'GET');
workbox.registerRoute(/.*/i, new workbox.NetworkOnly({
"cacheName": "dev",
plugins: []
}), 'GET');
}));
This diff is collapsed.
"use client";
import { useState } from "react";
export default function AbsensiPage() {
const [waktu] = useState(new Date().toLocaleString());
const handleAbsen = () => {
alert("Absen berhasil pada " + waktu);
};
return (
<main className="p-4">
<h2 className="text-xl font-bold mb-2">Halaman Absensi</h2>
<p>Waktu: {waktu}</p>
<button
onClick={handleAbsen}
className="bg-blue-500 text-white px-4 py-2 rounded mt-4"
>
Absen Sekarang
</button>
</main>
);
}
'use client';
import { useEffect, useState } from 'react';
import usePresenceStore from '@/stores/usePresenceStore';
import { Avatar, Col, Flex, Image, Row } from 'antd';
import { LogIn, LogOut } from 'lucide-react';
import LoadingComponent from '@/components/LoadingComponent';
import DetailPresenceCardComponent from '@/components/DetailPresenceCardComponent';
export default function AbsensiDetail() {
const presenceData = usePresenceStore((state) => state.presenceData);
const clearPresenceData = usePresenceStore((state) => state.clearPresenceData);
const [loading, setLoading] = useState(true);
// Cek apakah presenceData sudah tersedia
useEffect(() => {
if (presenceData) {
setLoading(false);
}
}, [presenceData]);
function getDayName(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', { weekday: 'long' });
}
function formatTanggalIndo(dateString) {
const tanggal = new Date(dateString);
return tanggal.toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
console.log(presenceData, 'data')
if (loading) {
return <LoadingComponent />;
}
if (!presenceData) {
return <div className='p-4 text-red-500'>Data kehadiran tidak tersedia. Silakan kembali ke halaman utama.</div>;
}
return (
<div className='pb-20'>
<div className='font-bold card-head bg-white flex justify-center'>
<div className='my-4 '> Detail Kehadiran</div>
</div>
<div className='p-4'>
<div className='card-shadow mt-2 p-4 rounded bg-white'>
<div>
{getDayName(presenceData.date) + ', ' + formatTanggalIndo(presenceData.date)}
</div>
<Row className="gutter-row mt-4" span={12}>
<Col span={12}>
<Flex align='center' gap={'small'}>
<Avatar size="middle" src={<LogIn size={16} style={{ color:"#443627" }} />} style={{ background: '#EFDCAB'}} />
<div className='text-md'>
{presenceData.presences?.[0]?.time || '00:00:00'} WIB
</div>
</Flex>
</Col>
<Col span={12}>
<Flex align='center' gap={'small'}>
<Avatar size="middle" src={<LogOut size={16} style={{ color:"#443627", marginLeft: 4 }} />} style={{ background: '#EFDCAB'}} />
<div className='text-md'>
{presenceData.presences?.[0]?.presence?.time || '00:00:00'} WIB
</div>
</Flex>
</Col>
</Row>
</div>
<div className='card-shadow mt-8'>
<div className="mb-4">
Datang
</div>
<div className="flex mb-2">
<div className="w-24 flex-none">Waktu</div>
<div className="w-4 flex-initial">:</div>
<div className="w-full flex-auto">{presenceData.presences?.[0]?.time || '00:00:00'} WIB</div>
</div>
<div className="flex mb-2">
<div className="w-24 flex-none">Lokasi</div>
<div className="w-4 flex-initial">:</div>
<div className="w-full flex-auto">{presenceData.presences?.[0]?.location_name || '-'}</div>
</div>
<div className="flex mb-2">
<div className="w-24 flex-none">Catatan</div>
<div className="w-4 flex-initial">:</div>
<div className="w-full flex-auto">{presenceData.presences?.[0]?.add_information_condition || '-'}</div>
</div>
<div className="flex mb-2">
<div className="w-24 flex-none">Foto</div>
<div className="w-4 flex-initial">:</div>
<div className="w-full flex-auto">
<Image
src={presenceData.presences[0]?.picture ||
'/no-image.png'}
/>
</div>
</div>
</div>
<div className='card-shadow mt-8'>
<div className="mb-4">
Pulang
</div>
<div className="flex mb-2">
<div className="w-24 flex-none">Waktu</div>
<div className="w-4 flex-initial">:</div>
<div className="w-full flex-auto">{presenceData.presences?.[0]?.presence?.time || '00:00:00'} WIB</div>
</div>
<div className="flex mb-2">
<div className="w-24 flex-none">Lokasi</div>
<div className="w-4 flex-initial">:</div>
<div className="w-full flex-auto">{presenceData.presences?.[0]?.presence?.location_name || '-'}</div>
</div>
<div className="flex mb-2">
<div className="w-24 flex-none">Catatan</div>
<div className="w-4 flex-initial">:</div>
<div className="w-full flex-auto">{presenceData.presences?.[0]?.presence?.add_information_condition || '-'}</div>
</div>
<div className="flex mb-2">
<div className="w-24 flex-none">Foto</div>
<div className="w-4 flex-initial">:</div>
<div className="w-full flex-auto">
<Image
src={presenceData.presences?.[0]?.presence?.picture || '/no-image.png'}
/>
</div>
</div>
</div>
{/* <div className='card-shadow mt-8'>
<div className='font-bold'>Pulang</div>
<div className='mt-4'>
<div className="grid grid-cols-2">
<div className=' cols'>Waktu</div>
<div>{presenceData.presences[0]?.presence?.time || '-'} WIB</div>
</div>
<div className="grid grid-cols-2 mt-4">
<div className=' cols'>Lokasi</div>
<div>{presenceData.presences?.[0]?.presence?.location_name || '-'}</div>
</div>
<div className="grid grid-cols-2 mt-4">
<div className=' cols'>Catatan</div>
<div>{presenceData.presences?.[0]?.presence?.add_information_type || '-'}</div>
</div>
<div className='mt-4'>
<Image
width={200}
src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png"
/>
</div>
</div>
</div> */}
</div>
</div>
);
}
'use client'
import DetailPresenceCardComponent from "@/components/DetailPresenceCardComponent";
import LoadingComponent from "@/components/LoadingComponent";
import axiosInstance from "@/lib/axios";
import { useSession } from "next-auth/react"
import { useEffect, useState } from "react";
export default function attendance(){
const {data : session} = useSession()
const [dataAbsenHistory, setDataAbsenHistory] = useState([]);
const [loadingAbsenHistory, setLoadingAbsenHistory] = useState(true);
useEffect(() => {
if (session?.accessToken) {
getDataAbsenHistory(session.accessToken);
}
}, [session]);
const getDataAbsenHistory = async (token) => {
setLoadingAbsenHistory(true);
try {
const response = await axiosInstance.get('/user/get-absen-history-parent?limit=31&page=1', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const fetchedData = response?.data?.data ?? [];
setDataAbsenHistory(fetchedData.data);
} catch (error) {
console.error('Gagal mengambil data riwayat absen:', error);
}
setLoadingAbsenHistory(false);
};
return(
<div className="min-h-screen">
<div className="p-4 card-head">
Kehadiran
</div>
<div className="p-4">
{loadingAbsenHistory ? <><LoadingComponent /></> : dataAbsenHistory.map((item) => (
<DetailPresenceCardComponent
key={item.id}
date={item.date}
presences={item.presences}
/>
))}
</div>
</div>
)
}
\ No newline at end of file
'use client';
import DetailPresenceCardComponent from '@/components/DetailPresenceCardComponent';
import LoadingComponent from '@/components/LoadingComponent';
import PresenceCard from '@/components/PresenceCardComponent';
import ProfileCard from '@/components/ProfileCardComponent';
import axiosInstance from '@/lib/axios';
import { useSession } from 'next-auth/react';
import React, { useEffect, useState } from 'react';
import dayjs from 'dayjs';
import 'dayjs/locale/id';
import { useRouter } from 'next/navigation';
dayjs.locale('id');
export default function Beranda() {
const {data : session} = useSession();
const [dataAbsenToday, setDataAbsenToday] = useState([]);
const [dataAbsenHistory, setDataAbsenHistory] = useState([]);
const [loadingAbsenHistory, setLoadingAbsenHistory] = useState(true);
const [loadingAbsenToday, setLoadingAbsenToday] = useState(true);
const [typePresenceIn, setTypePresenceIn] = useState('Datang');
const [typePresenceOut, setTypePresenceOut] = useState('Pulang');
const router = useRouter();
useEffect(() => {
if (session?.accessToken) {
getDataAbsenToday(session.accessToken);
getDataAbsenHistory(session.accessToken);
}
}, [session]);
const getDataAbsenToday = async (token) => {
setLoadingAbsenToday(true);
try {
const response = await axiosInstance.get('/user/get-absen-today-parent', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const fetchedData = response?.data ?? [];
const absenData = fetchedData.data[0];
setDataAbsenToday(fetchedData.data[0]);
console.log(absenData);
if (absenData?.presence_type === 'masuk') {
document.cookie = 'hasCheckedIn=true; path=/;';
} else {
document.cookie = 'hasCheckedIn=; Max-Age=0; path=/;'; // hapus cookie jika belum absen
}
if (absenData?.presence?.presence_type === 'pulang') {
document.cookie = 'hasCheckedOut=true; path=/;';
} else {
document.cookie = 'hasCheckedOut=; Max-Age=0; path=/;'; // hapus cookie jika belum absen
}
} catch (error) {
console.error('Gagal mengambil data absen today :', error);
}
setLoadingAbsenToday(false);
}
const getDataAbsenHistory = async (token) => {
setLoadingAbsenHistory(true);
try {
const response = await axiosInstance.get('/user/get-absen-history-parent?limit=4&page=1', {
headers: {
Authorization: `Bearer ${token}`,
},
});
const fetchedData = response?.data?.data ?? [];
setDataAbsenHistory(fetchedData.data);
} catch (error) {
console.error('Gagal mengambil data riwayat absen:', error);
}
setLoadingAbsenHistory(false);
};
const handlePage = () => {
setLoadingAbsenHistory(true);
router.push('/attendance');
setLoadingAbsenHistory(false);
}
return (
<div className="min-h-screen pb-20">
{/* <button onClick={() => {
document.cookie = 'hasCheckedIn=; Max-Age=0; path=/;';
}}>
Reset Status Check-In
</button> */}
<div className='p-4 bg-header-custom'>
<ProfileCard
name="Andi Saputra"
image="/bg/profile.jpg"
/>
</div>
<div className='p-4' style={{ marginTop: -120 }}>
<div className='card mt-8' style={{ backgroundColor: '#D98324' }}>
<div className='text-white'>
Kehadiran Hari Ini
<div className='text-sm'>
{dayjs().format('dddd, D MMMM YYYY')}
</div>
</div>
<div className="grid grid-cols-2 gap-4 ">
<PresenceCard presenceType={typePresenceIn} presenceData={dataAbsenToday}/>
<PresenceCard presenceType={typePresenceOut} presenceData={dataAbsenToday}/>
</div>
</div>
<div className="grid gap-4 mt-4">
<div className="col-start font-bold">Riwayat Kehadiran</div>
<div className="col-end-3 text-end text-blue-600">
<div onClick={handlePage}>
Lihat Semua
</div>
</div>
</div>
<div>
{loadingAbsenHistory ? <><LoadingComponent /></> : dataAbsenHistory.map((item) => (
<DetailPresenceCardComponent
key={item.id}
date={item.date}
presences={item.presences}
/>
))}
</div>
</div>
</div>);
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
src/app/favicon.ico

25.3 KB | W: | H:

src/app/favicon.ico

22 KB | W: | H:

src/app/favicon.ico
src/app/favicon.ico
src/app/favicon.ico
src/app/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
'use client';
import { useEffect, useState } from 'react';
import { Button, message } from 'antd';
import { useRouter } from 'next/navigation';
import CameraComponent from '@/components/CameraComponent';
import axiosInstance from '@/lib/axios';
import { useSession } from 'next-auth/react';
import CameraFaceComponent from '@/components/CameraFaceComponent';
export default function FotoPage() {
const [dataAbsen, setDataAbsen] = useState(null);
const [imageSrc, setImageSrc] = useState(null);
const [messageApi, contextHolder] = message.useMessage();
const router = useRouter();
const [absenTime, setAbsenTime] = useState(null);
const { data: session } = useSession();
useEffect(() => {
const isFirstLoad = sessionStorage.getItem('fotoPageLoaded');
if (!isFirstLoad) {
const absen = localStorage.getItem('pendingAbsensi');
if (absen) {
sessionStorage.setItem('fotoPageLoaded', 'true');
setDataAbsen(JSON.parse(absen));
setAbsenTime(new Date());
} else {
router.back();
}
} else {
localStorage.removeItem('pendingAbsensi');
sessionStorage.removeItem('fotoPageLoaded');
router.back();
}
}, []);
const dataURLtoBlob = (dataURL) => {
const arr = dataURL.split(",");
const mime = arr[0].match(/:(.*?);/)[1];
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
};
const handleSubmit = async () => {
try {
const blob = dataURLtoBlob(imageSrc);
const formData = new FormData();
formData.append('picture', blob);
// formData.append('picture', imageSrc); // base64 string
formData.append('latitude_longitude', dataAbsen.latitude_longitude);
formData.append('presence_type', dataAbsen.presence_type);
formData.append('presence_condition', dataAbsen.presence_condition);
formData.append('add_information_type', dataAbsen.add_information_type);
formData.append('add_information_condition', dataAbsen.add_information_condition === "" ? '-' : dataAbsen.add_information_condition);
formData.append('location_validation_status', dataAbsen.location_validation_status);
formData.append('office_id', dataAbsen.office_id);
formData.append('office_name', dataAbsen.office_name);
formData.append('location_name', dataAbsen.location_name);
await axiosInstance.post('/user/presence', formData, {
headers: {
'Authorization': `Bearer ${session?.accessToken}`,
'Content-Type': 'multipart/form-data',
},
});
localStorage.removeItem('pendingAbsensi');
sessionStorage.removeItem('fotoPageLoaded'); // tambahkan ini
messageApi.success('Presensi berhasil dikirim');
router.push('/beranda');
} catch (err) {
console.error(err);
messageApi.error('Gagal mengirim data');
}
};
return (
<div className="p-4">
{contextHolder}
<h1 className="text-lg font-bold mb-4">Ambil Foto Presensi</h1>
{absenTime && (
<p className="text-center mt-2 text-black-600">
Waktu Absen: {absenTime.toLocaleString('id-ID', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
day: '2-digit',
month: 'short',
year: 'numeric'
})}
</p>
)}
<div className='mt-4'>
{/* <CameraComponent onCapture={(img) => setImageSrc(img)}/> */}
<CameraFaceComponent onCapture={(img) => setImageSrc(img)}/>
</div>
{imageSrc && (
<Button type="primary" block className="mt-4" onClick={handleSubmit}>
Kirim Presensi
</Button>
)}
</div>
);
}
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
......
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
import '../../styles/globals.css';
import { AntdRegistry } from '@ant-design/nextjs-registry';
import React from 'react';
import { DM_Sans } from 'next/font/google'
import { ConfigProvider } from 'antd';
import theme from '../../theme.config';
import '@ant-design/v5-patch-for-react-19';
import ConditionalBottomMenu from '@/components/ConditionalBottomMenu';
import { SessionProvider } from 'next-auth/react';
import SessionWrapper from '@/providers/SessionWrapper';
export const metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Absensi PWA",
description: "Aplikasi Absensi berbasis PWA",
manifest: "/manifest.json",
};
const dmSans = DM_Sans({
subsets: ['latin'],
weight: ['400', '500', '700'], // pilih yang kamu butuhkan
variable: '--font-dm-sans', // optional, jika ingin pakai CSS variable
display: 'swap',
})
export default function RootLayout({ children }) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<html lang="id" >
<head>
<link rel="manifest" href="/manifest.json" />
<link rel="icon" href="/icons/icon-192x192.png" />
<meta name="theme-color" content="#0f172a" />
</head>
<body>
<SessionWrapper>
<ConfigProvider theme={theme}>
<AntdRegistry>
<div className={dmSans.className}>
<div className="relative w-full md:max-w-md md:mx-auto min-h-screen shadow background-body">
<div className=''>
{children}
</div>
<ConditionalBottomMenu />
</div>
</div>
</AntdRegistry>
</ConfigProvider>
</SessionWrapper>
</body>
</html>
);
}
'use client'
import React, { useEffect, useState } from 'react';
import { Button, Form, Input } from 'antd';
import { signIn } from 'next-auth/react';
import { useRouter, useSearchParams } from 'next/navigation';
export default function Login() {
const router = useRouter();
const searchParams = useSearchParams();
const error = searchParams.get('error');
const [loading, setLoading] = useState(false);
useEffect(() => {
if (error) {
alert("Login gagal: " + error);
}
}, [error]);
const handleSubmit = async (values) => {
setLoading(true);
const result = await signIn("credentials", {
username: values.username,
password: values.password,
redirect: false,
});
if (result.ok) {
router.push("/beranda");
} else {
alert("Login gagal");
}
setLoading(false);
};
return (
<div className="justify-center p-4">
<div className="pt-[155px]">
<Form
initialValues={{ remember: true }}
onFinish={handleSubmit}
autoComplete="off"
>
<Form.Item
name="username"
rules={[{ required: true, message: 'Please input your username!' }]}
>
<Input placeholder="Email" />
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your password!' }]}
>
<Input.Password placeholder="Password" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" block loading={loading}>
Submit
</Button>
</Form.Item>
</Form>
</div>
</div>
);
}
import Image from "next/image";
'use client';
import Login from './login/page';
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.js
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
return (
<div>
<Login />
</div>
);
}
import LogoutButton from "@/components/ButtonLogout";
export default function Profile() {
return(
<div className="p-4">
<LogoutButton />
</div>
)
}
\ No newline at end of file
'use client';
import { usePathname, useRouter } from 'next/navigation';
import { CalendarCheck, Home, User } from 'lucide-react';
import classNames from 'classnames';
export default function BottomMenuComponent() {
const pathname = usePathname();
const router = useRouter();
const menus = [
{
label: 'Beranda',
icon: (active) => <Home size={20} className={active ? 'text-blue-600' : 'text-gray-500'} />,
path: '/beranda',
},
{
label: 'Kehadiran',
icon: (active) => <CalendarCheck size={20} className={active ? 'text-blue-600' : 'text-gray-500'} />,
path: '/attendance',
},
{
label: 'Profil',
icon: (active) => <User size={20} className={active ? 'text-blue-600' : 'text-gray-500'} />,
path: '/profile',
},
];
return (
<div className="fixed bottom-0 left-0 right-0 z-40 bg-white border-t border-gray-200 shadow-md">
<div className="flex justify-around items-center h-16">
{menus.map((menu) => {
const isActive = pathname === menu.path;
return (
<button
key={menu.path}
onClick={() => router.push(menu.path)}
className={classNames(
'flex flex-col items-center justify-center text-sm',
isActive ? 'text-blue-600 font-semibold' : 'text-gray-500'
)}
>
{menu.icon(isActive)}
<span className="text-xs mt-1">{menu.label}</span>
{isActive && <div className="w-1.5 h-1.5 bg-blue-600 rounded-full mt-1" />}
</button>
);
})}
</div>
</div>
);
}
'use client';
import { Button } from "antd";
import { signOut, useSession } from "next-auth/react";
import ProfileCardComponent from "./ProfileCardComponent";
export default function LogoutButton() {
const { data: session } = useSession();
console.log(session);
return (
<div>
<div className="card bg-black">
<ProfileCardComponent />
</div>
<div className="mt-4">
<Button type="primary"
onClick={() => signOut({ callbackUrl: "/" })}
// className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded"
>
Logout
</Button>
</div>
</div>
);
}
'use client';
import { useRef, useState } from 'react';
import Webcam from 'react-webcam';
import { Button, message } from 'antd';
// Fungsi kompresi gambar agar < 500KB
export const compressImage = async (base64, maxSizeKB = 500, quality = 0.7) => {
return new Promise((resolve) => {
const img = new Image();
img.src = base64;
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
// Flip secara horizontal (mirror)
ctx.translate(canvas.width, 0);
ctx.scale(-1, 1);
ctx.drawImage(img, 0, 0);
let compressedBase64 = canvas.toDataURL('image/jpeg', quality);
let sizeInKB = Math.round((compressedBase64.length * (3 / 4)) / 1024);
// Loop jika masih > maxSizeKB
while (sizeInKB > maxSizeKB && quality > 0.1) {
quality -= 0.05;
compressedBase64 = canvas.toDataURL('image/jpeg', quality);
sizeInKB = Math.round((compressedBase64.length * (3 / 4)) / 1024);
}
resolve(compressedBase64);
};
});
};
export default function CameraComponent({ onCapture }) {
const webcamRef = useRef(null);
const [imageSrc, setImageSrc] = useState(null);
const [loading, setLoading] = useState(false);
const [messageApi, contextHolder] = message.useMessage();
const capture = async () => {
if (!webcamRef.current) return;
const screenshot = webcamRef.current.getScreenshot();
if (!screenshot) {
messageApi.error('Gagal mengambil gambar');
return;
}
setLoading(true);
const compressed = await compressImage(screenshot);
setImageSrc(compressed);
setLoading(false);
onCapture(compressed);
};
const reset = () => {
setImageSrc(null);
onCapture(null);
};
return (
<div>
{!imageSrc ? (
<>
<Webcam
ref={webcamRef}
audio={false}
screenshotFormat="image/jpeg"
width="100%"
videoConstraints={{ facingMode: 'user' }}
style={{ transform: 'scaleX(-1)' }}
/>
<Button type="primary" block onClick={capture} className="mt-4" loading={loading}>
Ambil Foto
</Button>
</>
) : (
<>
<img
src={imageSrc}
alt="Hasil Foto"
className="w-64 h-64 object-cover border rounded mx-auto"
/>
<Button type="primary" block className="mt-4" onClick={reset}>
Ulangi Foto
</Button>
</>
)}
</div>
);
}
'use client';
import { useEffect, useRef, useState } from 'react';
import Webcam from 'react-webcam';
import { Button, message } from 'antd';
import { loadFaceApi } from '@/utils/loadFaceApi';
export default function CameraFaceComponent({ onCapture }) {
const webcamRef = useRef(null);
const canvasRef = useRef(null);
const [faceDetected, setFaceDetected] = useState(false);
const [imageSrc, setImageSrc] = useState(null);
const [messageApi, contextHolder] = message.useMessage();
const intervalRef = useRef(null);
useEffect(() => {
initFaceDetection();
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, []);
const initFaceDetection = async () => {
const faceapi = await loadFaceApi();
intervalRef.current = setInterval(async () => {
if (
webcamRef.current &&
webcamRef.current.video &&
webcamRef.current.video.readyState === 4
) {
const video = webcamRef.current.video;
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
const displaySize = {
width: video.videoWidth,
height: video.videoHeight,
};
// Samakan ukuran canvas dengan video
canvas.width = displaySize.width;
canvas.height = displaySize.height;
const detections = await faceapi.detectAllFaces(
video,
new faceapi.TinyFaceDetectorOptions({
inputSize: 416,
scoreThreshold: 0.5,
})
);
setFaceDetected(detections.length > 0);
const resized = faceapi.resizeResults(detections, displaySize);
// Gambar bounding box mirror
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.scale(-1, 1);
ctx.translate(-canvas.width, 0);
faceapi.draw.drawDetections(canvas, resized);
ctx.restore();
}
}, 400);
};
const capture = () => {
const screenshot = webcamRef.current.getScreenshot();
setImageSrc(screenshot);
onCapture(screenshot);
};
const reset = () => {
setImageSrc(null);
onCapture(null);
};
return (
<div className="relative w-full">
{contextHolder}
{!imageSrc ? (
<>
<div className="relative w-full">
<Webcam
ref={webcamRef}
audio={false}
screenshotFormat="image/jpeg"
width="100%"
videoConstraints={{
facingMode: 'user',
width: { ideal: 640 },
height: { ideal: 480 }
}}
// style={{ transform: 'scaleX(-1)' }}
/>
<canvas
ref={canvasRef}
className="absolute top-0 left-0 z-10"
style={{
transform: 'scaleX(-1)',
width: '100%',
height: '100%',
}}
/>
</div>
<div className="text-center mt-2 text-sm">
{faceDetected ? (
<span className="text-green-600">Wajah terdeteksi </span>
) : (
<span className="text-red-500">Arahkan wajah ke kamera </span>
)}
</div>
<Button
type="primary"
block
className="mt-4"
onClick={capture}
disabled={!faceDetected}
>
Ambil Foto
</Button>
</>
) : (
<>
<img
src={imageSrc}
alt="Foto"
className="w-64 h-64 object-cover mx-auto border rounded"
/>
<Button block className="mt-4" onClick={reset}>
Ulangi Foto
</Button>
</>
)}
</div>
);
}
'use client';
import { usePathname } from 'next/navigation';
import BottomMenuComponent from './BottomMenuComponent';
export default function ConditionalBottomMenu() {
const pathname = usePathname();
if (['/', '/login', '/register'].includes(pathname)) return null;
return <BottomMenuComponent />;
}
import React from 'react';
import { Card, Avatar, Row, Col, Flex, Badge, Button } from 'antd';
import { CloseCircleOutlined, LoginOutlined, LogoutOutlined } from '@ant-design/icons';
import { LogIn, LogOut } from 'lucide-react';
import {useRouter} from 'next/navigation';
import usePresenceStore from '@/stores/usePresenceStore';
const { Meta } = Card;
export default function DetailPresenceCardComponent({ date, presences }) {
const router = useRouter();
const setPresenceData = usePresenceStore((state) => state.setPresenceData);
function getDayName(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('id-ID', { weekday: 'long' }); // Output: "Jumat"
}
function formatTanggalIndo(dateString) {
const tanggal = new Date(dateString);
return tanggal.toLocaleDateString('id-ID', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
}
const handleClick = () => {
setPresenceData({ date, presences });
router.push('/attendance/detail');
}
const checkDayWork = (date, presences) => {
if (getDayName(date) === 'Sabtu' || getDayName(date) === 'Minggu'){
if (!presences[0]) {
return 'Hari Libur'
} else {
return 'Hari Kerja'
}
} else {
return 'Hari Kerja'
}
}
// getDayName(date) === 'Sabtu' || getDayName(date) === 'Minggu' ? !presences[0] ? 'Hari Libur' : 'Hari Kerja' : 'Hari Kerja'
return (
<div className='mt-4' >
<Badge.Ribbon text={getDayName(date)}
style={checkDayWork(date, presences) === 'Hari Kerja' ?
{ width: 80, textAlign: 'center', backgroundColor:'#D98324' } :
{ width: 80, textAlign: 'center', backgroundColor:'#D98324' }
}
>
<div className='card-shadow cursor-pointer hover:shadow-lg transition'
onClick={handleClick}>
<div className='font-bold'>
{formatTanggalIndo(date)}
</div>
<div className='mt-4 mb-4'>
<Row className="gutter-row" span={12}>
<Col span={12}>
<Flex align='center' gap={'small'}>
<Avatar size="middle" src={<LogIn size={16} style={{ color:"#443627" }} />} style={{ background: '#EFDCAB'}} />
<div className='text-md'>{presences[0]?.time || '00:00:00'} WIB</div>
</Flex>
</Col>
<Col span={12}>
<Flex align='center' gap={'small'}>
<Avatar size="middle" src={<LogOut size={16} style={{ color:"#443627", marginLeft: 4 }} />} style={{ background: '#EFDCAB'}} />
<div className='text-md'>{presences[0]?.presence ? presences[0]?.presence?.time : '00:00:00'} WIB</div>
</Flex>
</Col>
</Row>
</div>
</div>
</Badge.Ribbon>
</div>
);
}
'use client';
import { MapContainer, TileLayer, Marker, Popup, Circle } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconUrl: '/leaflet/marker-icon.png',
iconRetinaUrl: '/leaflet/marker-icon-2x.png',
shadowUrl: '/leaflet/marker-shadow.png',
});
/**
* Komponen peta lokasi real-time
* @param {number} lat - Latitude user
* @param {number} lng - Longitude user
* @param {number} radius - Radius validasi (dalam meter)
* @param {number} centerLat - Titik tengah area kantor (optional)
* @param {number} centerLng - Titik tengah area kantor (optional)
*/
export default function LiveLocationMap({ lat, lng, radius, centerLat, centerLng }) {
if (!lat || !lng) return <div>Loading map...</div>;
return (
<MapContainer center={[lat, lng]} zoom={17} style={{ height: '300px', width: '100%' }}>
<TileLayer
attribution='&copy; OpenStreetMap contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
{/* Marker posisi user */}
<Marker position={[lat, lng]}>
<Popup>Lokasi Anda</Popup>
</Marker>
{/* Circle radius validasi */}
{radius && centerLat && centerLng && (
<Circle
center={[centerLat, centerLng]}
radius={radius}
pathOptions={{ fillColor: 'blue', fillOpacity: 0.1, color: 'blue' }}
/>
)}
</MapContainer>
);
}
import { LoadingOutlined } from '@ant-design/icons';
import { Spin } from 'antd';
export default function LoadingComponent() {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-white">
<Spin
size="large"
/>
</div>
);
}
import React, { useEffect, useState } from 'react';
import { Card, Avatar, Row, Col, Flex, Badge, Button, Tag } from 'antd';
import Icon, { CloseCircleOutlined, CloseCircleTwoTone, LoginOutlined, LogoutOutlined } from '@ant-design/icons';
import { LogIn, LogOut } from 'lucide-react';
import { useRouter } from 'next/navigation';
const { Meta } = Card;
export default function PresenceCardComponent({ presenceType, presenceUrl, presenceData }) {
const router = useRouter();
const handleClick = (type) => {
if(type === 'Datang'){
router.push('/check-in');
} else {
router.push('/check-out');
}
}
// console.log(presenceData?.presence?.time)
return (
<div className='mt-4'>
<div className='card bg-white'>
<div>
<Flex justify='flex-start' align='center' gap={'small'}>
<div className='max-w-sm'>
{!presenceUrl ?
presenceType === 'Datang' ?
<Avatar size="middle" src={<LogIn size={16} style={{ color:"#443627" }} />} style={{ background: '#EFDCAB'}} />
:
<Avatar size="middle" src={<LogOut size={16} style={{ color:"#443627", marginLeft: 4 }} />} style={{ background: '#EFDCAB'}} />
:
<Avatar shape='square' size="middle" src="/bg/prof.jpg" />
}
</div>
<div className='text-sm'>
{presenceType === 'Datang' ?
<div className='font-bold text-sm'>{presenceData?.time || '00:00'} WIB</div>
:
<div className='font-bold text-sm'>{presenceData?.presence?.time || '00:00'} WIB</div>
}
</div>
</Flex>
<div className='mt-2'>
{presenceType === "Datang"
?
(
<Button size='small' type='primary' onClick={() => handleClick(presenceType)} block disabled={presenceData?.presence_type === 'masuk'}>
<div className='text-sm'>
Datang
</div>
</Button>
) :
(
<Button size='small' type='primary' onClick={() => handleClick(presenceType)} block disabled={presenceData?.presence?.presence_type === 'pulang' || !presenceData ? true : false}>
<div className='text-sm'>
Pulang
</div>
</Button>
)
}
</div>
</div>
</div>
</div>
);
}
import React from 'react';
import { Card, Avatar, Flex } from 'antd';
import { useSession } from 'next-auth/react';
const { Meta } = Card;
export default function ProfileCardComponent({ name, image }) {
const { data: session } = useSession();
// console.log(session);
return (
<div className='text-white'>
<div className='flex justify-between'>
<div>
<div className='font-bold '>{session?.user?.name || '-'}</div>
<div>{session?.user?.nip || 'NIP'}</div>
</div>
<Avatar style={{ border: '2px solid #D98324' }} size={'large'} src={"/bg/prof.jpg"} />
</div>
</div>
);
}
This diff is collapsed.
import axios from 'axios';
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
Accept: 'application/json',
},
});
export default axiosInstance;
import { getToken } from "next-auth/jwt";
import { NextResponse } from "next/server";
export async function middleware(req) {
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
const nowInSeconds = Math.floor(Date.now() / 1000);
const isTokenValid = token && token.exp && token.exp > nowInSeconds;
const isAuth = !!token && isTokenValid;
const currentPath = req.nextUrl.pathname;
const isLoginPage = ["/", "/login"].includes(currentPath);
// ❌ Tidak login atau token kadaluarsa → redirect ke /login
if (!isAuth && !isLoginPage) {
return NextResponse.redirect(new URL("/login", req.url));
}
// ✅ Sudah login tapi buka / atau /login → redirect ke /beranda
if (isAuth && isLoginPage) {
return NextResponse.redirect(new URL("/beranda", req.url));
}
// 🔁 Sudah check-in → redirect dari /check-in
if (currentPath === "/check-in") {
const hasCheckedIn = req.cookies.get("hasCheckedIn")?.value;
if (hasCheckedIn === "true") {
return NextResponse.redirect(new URL("/", req.url));
}
}
// 🔁 Sudah check-out → redirect dari /check-out
if (currentPath === "/check-out") {
const hasCheckedOut = req.cookies.get("hasCheckedOut")?.value;
if (hasCheckedOut === "true") {
return NextResponse.redirect(new URL("/", req.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: [
"/",
"/login",
"/beranda/:path*",
"/profile/:path*",
"/attendance/:path*",
"/check-in",
"/check-out",
],
};
This diff is collapsed.
// src/providers/SessionWrapper.js
'use client';
import { SessionProvider } from 'next-auth/react';
export default function SessionWrapper({ children }) {
return <SessionProvider>{children}</SessionProvider>;
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment