change layout and add component

This commit is contained in:
Md Afiq Iskandar 2024-08-28 07:44:52 +08:00
parent 46b84b1535
commit 9662587eda
32 changed files with 12719 additions and 348 deletions

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 353 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 16 KiB

7126
assets/json/iconamoon.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
html[data-theme="default"] {
--color-primary: 13, 27, 42;
--color-primary: 0, 165, 154;
--color-secondary: 97, 176, 183;
--color-accent: 243, 244, 246;
--color-success: 79, 192, 103;
@ -10,6 +10,11 @@ html[data-theme="default"] {
--border-color: 228, 228, 231;
--bg-1: 243, 244, 246;
--bg-2: 255, 255, 255;
--sidebar: 38, 50, 55;
--sidebar-menu: 26, 35, 38;
--sidebar-text: 255, 255, 255;
--header: 49, 65, 71;
--header-text: 255, 255, 255;
--scroll-color: 170, 170, 170;
--scroll-hover-color: 155, 155, 155;
--fk-border-color: 228, 228, 231;
@ -123,4 +128,4 @@ html[data-theme="custom1"] {
--tab-border: 1px;
--tab-radius: 0.5rem;
--tw-shadow: #e5eaf2;
}
}

View File

@ -26,3 +26,7 @@
.badge.badge-danger {
@apply bg-danger text-white;
}
.badge.badge-disabled {
@apply bg-gray-300 text-gray-600;
}

View File

@ -5,7 +5,8 @@ body {
.w-header {
@apply z-20 fixed top-0 right-0 px-5 py-3 duration-300 shadow-md;
background-color: rgb(var(--bg-2));
background-color: rgb(var(--header));
color: rgb(var(--header-text));
box-shadow: var(--box-shadow);
}
@ -16,7 +17,8 @@ body {
.vertical-menu {
@apply text-base h-screen fixed w-64 top-0 z-50 duration-300 border-l-0 shadow-md;
background-color: rgb(var(--bg-2));
background-color: rgb(var(--sidebar));
color: rgb(var(--sidebar-text));
box-shadow: var(--box-shadow);
}
@ -26,17 +28,13 @@ body {
justify-center
transition-colors
duration-300;
color: rgb(var(--text-color));
color: rgb(var(--header-text));
}
.icon-btn.profile {
@apply border;
border-radius: var(--rounded-box);
border: 1px solid rgb(var(--border-color));
color: rgb(var(--text-color));
color: rgb(var(--header-text));
}
.icon-btn:hover {
color: rgb(var(--text-color));
background-color: rgb(var(--bg-1));
background-color: rgb(var(--sidebar));
}

View File

@ -2,17 +2,18 @@
Notes: This file is the main entry point for the SCSS stylesheet.
================================================================================*/
@import url("https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@300..700&display=swap");
html,
body {
height: 100%;
}
#__nuxt {
font-family: "Space Grotesk", sans-serif;
font-family: "Roboto", sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 400;
letter-spacing: 0.5px;
font-size: 14px;
}

View File

@ -15,12 +15,9 @@ const refreshPage = () => {
<div class="rs-loading bg-white absolute z-50 top-0 left-0 w-full h-full">
<div class="flex justify-center text-center items-center h-screen">
<div>
<div class="flex justify-center items-center">
<img
src="@/assets/img/logo/logo-nobg.svg"
class="w-[50px] mb-5"
alt=""
/>
<div class="img-container flex justify-center items-center mb-5">
<img src="@/assets/img/logo/niise-logo.svg" class="max-w-[60px]" />
<img src="@/assets/img/logo/niise-text.svg" class="max-w-[120px]" />
</div>
<div

View File

@ -25,6 +25,7 @@ const props = defineProps({
'badge-success': variant === 'success',
'badge-warning': variant === 'warning',
'badge-danger': variant === 'danger',
'badge-disabled': variant === 'disabled',
}"
>
<Icon v-if="icon" :name="icon" :size="iconSize"></Icon>

View File

@ -12,12 +12,17 @@ const props = defineProps({
type: String,
default: "md",
},
btnType: {
type: String,
default: "button",
},
});
</script>
<template>
<button
class="button"
:type="btnType"
:class="{
'button-sm': size === 'sm',
'button-md': size === 'md',

View File

@ -1,17 +1,39 @@
<script setup>
const route = useRoute();
// Get breadcrumb from page meta
const breadcrumb = computed(() => {
let breadcrumb = null;
const matched = route.matched;
const breadcrumb = matched.map((item) => {
return {
name: item.meta.title,
path: item.path,
};
});
if (matched[matched.length - 1].meta?.breadcrumb) {
breadcrumb = matched[matched.length - 1].meta.breadcrumb;
} else {
// if no breadcrumb in page meta, get breadcrumb from route matched
breadcrumb = matched.map((item) => {
return {
name: item.meta.title,
path: item.path,
};
});
return breadcrumb;
}
// if type current overwrite path to its own path
if (breadcrumb) {
breadcrumb.forEach((item) => {
if (item.type == "current") {
item.path = route.path;
} else if (item.type == "parent") {
item.path = route.path.split("/").slice(0, -item.parentNo).join("/");
}
});
}
return breadcrumb;
});
// Get title from page meta
const title = computed(() => {
const matched = route.matched;
const title = matched[matched.length - 1].meta.title;
@ -28,32 +50,43 @@ async function navigateMenu(path) {
</script>
<template>
<div
v-if="breadcrumb && title"
class="flex flex-col md:flex-row items-stretch justify-between pb-5"
>
<span class="text-xl font-semibold">{{ title }}</span>
<div
class="flex items-center text-sm"
v-if="breadcrumb && breadcrumb.length != 0"
>
<span
v-for="(item, index) in breadcrumb"
:key="index"
class="flex items-center text-primary"
>
<Icon
v-if="index != 0"
name="ic:round-chevron-right"
size="14"
class="pr-1"
></Icon>
<a
@click="navigateMenu(item.path)"
class="underline cursor-pointer hover:text-primary/90 pr-1"
>{{ item.name }}</a
<div v-if="breadcrumb" class="mb-6">
<nav aria-label="Breadcrumb" class="mb-4">
<ol class="flex items-center text-sm">
<li class="flex items-center">
<NuxtLink to="/" class="text-gray-500 hover:text-gray-700">
<Icon name="mdi:home" size="16" />
</NuxtLink>
</li>
<li
v-for="(item, index) in breadcrumb"
:key="index"
class="flex items-center"
>
</span>
</div>
<Icon
name="mdi:chevron-right"
size="16"
class="mx-2 text-gray-400"
aria-hidden="true"
/>
<a
@click="navigateMenu(item.path)"
:class="{
'text-gray-500 hover:text-gray-700':
index !== breadcrumb.length - 1,
'text-primary font-medium': index === breadcrumb.length - 1,
}"
:aria-current="index === breadcrumb.length - 1 ? 'page' : undefined"
>
{{ item.name }}
</a>
</li>
</ol>
</nav>
<!-- <div class="flex justify-between items-center">
<h1 class="text-2xl font-semibold text-gray-800">{{ title }}</h1>
<slot name="right"></slot>
</div> -->
</div>
</template>

View File

@ -85,60 +85,6 @@ onMounted(() => {
</div>
<div class="flex gap-2 item-center justify-items-end">
<VDropdown placement="bottom-end" distance="13" name="language">
<button class="icon-btn h-10 w-10 rounded-full">
<country-flag :country="languageNow.flagCode" />
</button>
<template #popper>
<ul class="header-dropdown w-full md:w-32">
<li
v-for="lang in langList"
class="flex items-center justify-center hover:bg-[rgb(var(--bg-1))]"
>
<button
@click="changeLanguage(lang.value)"
class="w-full py-2 px-2 flex justify-center items-center h-10"
>
<div class="ml-3 flex justify-center items-center">
<country-flag :country="lang.flagCode" />
</div>
<span class="grow">{{ lang.name }}</span>
</button>
</li>
</ul>
</template>
</VDropdown>
<VDropdown placement="bottom-end" distance="13" name="theme">
<button class="icon-btn h-10 w-10 rounded-full">
<Icon size="22px" name="material-symbols:format-paint-rounded" />
</button>
<template #popper>
<ul class="header-dropdown w-full md:w-52">
<li v-for="(val, index) in themes">
<a
@click="setTheme(val.theme)"
class="flex justify-between items-center cursor-pointer py-2 px-4 hover:bg-[rgb(var(--bg-1))]"
>
<span class="capitalize"> {{ val.theme }} </span>
<div class="flex items-center gap-x-1">
<div
v-for="(color, index) in val.colors"
class="h-[25px] w-[10px] rounded-lg"
:style="{
backgroundColor: rgbToHex(color.value),
}"
></div>
</div>
</a>
</li>
</ul>
</template>
</VDropdown>
<button @click="toggleSearch" class="icon-btn h-10 w-10 rounded-full">
<Icon name="ic:round-search" class="" />
</button>
<VDropdown placement="bottom-end" distance="13" name="notification">
<button class="relative icon-btn h-10 w-10 rounded-full">
<span
@ -202,7 +148,12 @@ onMounted(() => {
</template>
</VDropdown>
<VDropdown placement="bottom-end" distance="13" name="profile">
<VDropdown
placement="bottom-end"
distance="13"
name="profile"
class="flex justify-center item-center"
>
<button class="icon-btn profile px-2">
<img
class="w-8 h-8 object-cover rounded-full"
@ -212,10 +163,7 @@ onMounted(() => {
v-if="isDesktop"
class="grid grid-cols-1 text-left ml-3 flex-none"
>
<p class="font-semibold text-sm truncate w-24 mb-0">John Doe</p>
<span class="font-medium text-xs truncate w-24"
>RM 10,000.00</span
>
<p class="font-semibold text-sm truncate w-24 mb-0">Johan</p>
</div>
<Icon name="ic:outline-keyboard-arrow-down" class="ml-3" />
</button>

View File

@ -62,15 +62,8 @@ function openMenu(event) {
// Active menu
function activeMenu(routePath) {
return route.path == routePath
? ` shadow-lg
shadow-primary/50
dark:shadow-primary/10
text-white
bg-gradient-to-r
from-primary
to-primary/90
active-menu`
: `transition-all duration-300 hover:ml-4`;
? `bg-[rgb(var(--color-primary))] font-normal text-white active-menu`
: `font-light text-white/90 md:transition-all md:duration-200 hover:md:ml-2`;
}
function toggleMenu() {
@ -98,10 +91,10 @@ function navigationPage(path, external) {
v-if="item.header"
class="text-left font-normal text-xs mx-6 mt-5 mb-2"
>
<span class="uppercase text-primary dark:text-primary">
<span class="uppercase text-gray-400">
{{ item.header ? item.header : "" }}
</span>
<p class="text-gray-500 dark:text-gray-500">
<p class="text-gray-400">
{{ item.description ? item.description : "" }}
</p>
</div>
@ -130,7 +123,7 @@ function navigationPage(path, external) {
item2.child === undefined ||
(item2.child && item2.child.length === 0)
"
class="flex items-center px-4 py-3 mx-3 rounded-lg cursor-pointer"
class="flex items-center px-6 py-3 cursor-pointer"
@click="navigationPage(item2.path, item2.external)"
:class="activeMenu(item2.path)"
>
@ -146,7 +139,7 @@ function navigationPage(path, external) {
</NuxtLink>
<a
v-else
class="flex items-center px-4 py-3 mx-3 rounded-lg cursor-pointer"
class="flex items-center px-6 py-3 rounded-lg cursor-pointer"
:class="activeMenu(item2.path)"
>
<Icon v-if="item2.icon" :name="item2.icon" size="18"></Icon>

View File

@ -17,7 +17,7 @@ const props = defineProps({
},
indent: {
type: Number,
default: 0.2,
default: 0.5,
},
});
const emit = defineEmits(["openMenu"]);
@ -61,15 +61,8 @@ function openMenu(event) {
// Active menu
function activeMenu(routePath) {
return route.path == routePath
? ` shadow-lg
shadow-primary/50
dark:shadow-primary/10
text-white
bg-gradient-to-r
from-primary
to-primary/90
active-menu`
: `transition-all duration-300 hover:ml-4`;
? `bg-[rgb(var(--color-primary))] font-normal text-white active-menu`
: `font-light text-white/90 md:transition-all md:duration-200 hover:md:ml-2`;
}
function toggleMenu() {
@ -85,7 +78,7 @@ function navigationPage(path, external) {
}
const indentStyle = computed(() => {
return { "background-color": `rgba(var(--bg-1), ${indent.value})` };
return { "background-color": `rgba(var(--sidebar-menu), ${indent.value})` };
});
</script>
@ -113,7 +106,7 @@ const indentStyle = computed(() => {
v-if="
item.child === undefined || (item.child && item.child.length === 0)
"
class="flex items-center px-4 py-3 mx-3 rounded-lg cursor-pointer"
class="flex items-center px-6 py-3 cursor-pointer"
@click="navigationPage(item.path, item.external)"
:class="activeMenu(item.path)"
>
@ -128,7 +121,7 @@ const indentStyle = computed(() => {
</NuxtLink>
<a
v-else
class="flex items-center px-4 py-3 mx-3 rounded-lg cursor-pointer"
class="flex items-center px-6 py-3 rounded-lg cursor-pointer"
:class="activeMenu(item.path)"
>
<span class="mx-3 font-normal">{{ item.title }}</span>

View File

@ -31,14 +31,15 @@ onMounted(() => {
<template>
<div class="vertical-menu">
<div class="py-2 px-4">
<div class="py-2 px-4 bg-[rgb(var(--header))]">
<nuxt-link to="/">
<div class="flex flex-auto gap-3 justify-center items-center h-[48px]">
<img
class="h-10 block"
src="@/assets/img/logo/logo-word-black.svg"
class="h-8 block"
src="@/assets/img/logo/logo-imigresen.svg"
alt=""
/>
<span class="text-xs">Jabatan Imigresen Malaysia</span>
</div>
</nuxt-link>
</div>

View File

@ -1,6 +1,6 @@
export default [
{
header: "",
header: "Utama",
description: "",
child: [
{
@ -10,11 +10,64 @@ export default [
child: [],
meta: {},
},
{
title: "Carian",
path: "/carian",
icon: "ph:magnifying-glass",
child: [],
meta: {},
},
{
title: "Laporan",
path: "/laporan",
icon: "ph:chart-bar-fill",
child: [],
meta: {},
},
],
meta: {},
},
{
header: "Fungsi",
description: "",
child: [
{
title: "Komponen",
icon: "ph:gear-fine",
child: [
{
title: "Butang",
path: "/komponen/butang",
child: [],
meta: {},
},
{
title: "Modal",
path: "/komponen/modal",
child: [],
meta: {},
},
{
title: "Data Table",
path: "/komponen/datatable",
child: [],
meta: {},
},
],
},
{
title: "Ikon",
path: "/ikon",
icon: "iconamoon:slightly-smiling-face",
child: [],
meta: {},
},
],
meta: {},
},
{
header: "Administration",
description: "Manage your application",
description: "",
child: [
{
title: "Configuration",
@ -78,27 +131,4 @@ export default [
},
},
},
{
header: "Help",
description: "Help and documentation",
child: [
{
title: "Documentation",
icon: "solar:book-bookmark-minimalistic-bold",
path: "https://mawar-cms-docs.vercel.app",
external: true,
},
{
title: "UI Components",
icon: "material-symbols:settings-input-component-outline-rounded",
path: "https://corradui.datasc.dev",
external: true,
},
],
meta: {
auth: {
role: ["Developer"],
},
},
},
];

View File

@ -77,6 +77,7 @@
"vue3-apexcharts": "^1.4.1",
"vue3-click-away": "^1.2.4",
"vue3-dropzone": "^2.0.1",
"vue3-recaptcha-v2": "^2.0.2",
"vuedraggable": "^4.1.0"
}
}

122
pages/carian/index.vue Normal file
View File

@ -0,0 +1,122 @@
<script setup>
definePageMeta({
title: "Carian",
breadcrumb: [
{
name: "Carian",
type: "current",
},
],
});
const categories = ref([
"Semua Kategori",
"Kategori 1",
"Kategori 2",
"Kategori 3",
]);
const selectedCategory = ref("Semua Kategori");
const criteria = ref([
{ label: "Kriteria 1", options: ["Pilihan 1", "Pilihan 2", "Pilihan 3"] },
{ label: "Kriteria 2", options: ["Pilihan A", "Pilihan B", "Pilihan C"] },
{ label: "Kriteria 3", options: ["Pilihan X", "Pilihan Y", "Pilihan Z"] },
{ label: "Kriteria 4", options: ["Pilihan 1", "Pilihan 2", "Pilihan 3"] },
{
label: "Kriteria 5",
options: ["Pilihan A", "Pilihan B", "Pilihan C"],
},
]);
const searchQuery = ref("");
const performSearch = () => {
// Implement search logic here
console.log("Performing search with:", searchQuery.value);
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-card>
<template #header> Carian laman </template>
<template #body>
<FormKit type="form" @submit="performSearch" submit-label="Search">
<div class="flex items-center space-x-2 mb-4">
<FormKit
v-model="searchQuery"
type="text"
name="search"
placeholder="Carian..."
:classes="{
outer: 'flex-grow',
input: 'w-full',
}"
>
<template #prefixIcon>
<Icon
name="ph:magnifying-glass"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
<FormKit
type="select"
v-model="selectedCategory"
:options="categories"
placeholder="Semua Kategori"
:classes="{
outer: 'w-48',
}"
/>
</div>
<div class="text-lg font-medium text-gray-700 mb-4">
Carian Terperinci
</div>
<div class="grid grid-cols-2 gap-4 mb-4">
<FormKit
v-for="(item, index) in criteria.slice(0, 2)"
:key="index"
type="select"
:name="item.label.toLowerCase().replace(' ', '_')"
:label="item.label"
:options="item.options"
placeholder="Pilih pilihan"
>
<template #label>
<label
class="formkit-label text-gray-700 dark:text-gray-200 block mb-2 font-semibold text-sm formkit-invalid:text-red-500"
>
{{ item.label }}
</label>
</template>
</FormKit>
</div>
<div class="grid grid-cols-3 gap-4">
<FormKit
v-for="(item, index) in criteria.slice(2)"
:key="index"
type="select"
:name="item.label.toLowerCase().replace(' ', '_')"
:label="item.label"
:options="item.options"
placeholder="Pilih pilihan"
>
<template #label>
<label
class="formkit-label text-gray-700 dark:text-gray-200 block mb-2 font-semibold text-sm formkit-invalid:text-red-500"
>
{{ item.label }}
</label>
</template>
</FormKit>
</div>
</FormKit>
</template>
</rs-card>
</div>
</template>

View File

@ -3,6 +3,12 @@ definePageMeta({
title: "Dashboard",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Dashboard",
path: "/",
},
],
});
const data1 = ref([]);

View File

@ -1,9 +1,17 @@
<script setup>
definePageMeta({
title: "Forgot Password",
title: "Reset Password",
layout: "empty",
middleware: ["dashboard"],
});
const email = ref("");
const changePassword = () => {
// Simulate password change request without API call
console.log("Password change requested for email:", email.value);
// Add your password change logic here
};
</script>
<template>
@ -12,31 +20,55 @@ definePageMeta({
>
<div class="w-full md:w-3/4 lg:w-1/2 xl:w-2/6 relative">
<rs-card class="h-screen md:h-auto px-10 md:px-16 py-12 md:py-20 mb-0">
<div
class="absolute -bottom-3 left-3 img-container flex justify-start items-center mb-5"
>
<img
src="@/assets/img/logo/logo-word-black.svg"
class="max-w-[90px]"
/>
<div class="text-center mb-8">
<div class="img-container flex justify-center items-center mb-5">
<img src="@/assets/img/logo/niise-logo.svg" class="max-w-[60px]" />
<img src="@/assets/img/logo/niise-text.svg" class="max-w-[120px]" />
</div>
<h2 class="mt-4 text-2xl font-bold text-gray-700">
Tukar kata laluan
</h2>
<p class="text-sm text-gray-500">
Kata laluan sementara akan dihantar ke emel anda.
</p>
</div>
<h3 class="mb-4">Forgot Password</h3>
<p class="text-slate-500 mb-6">
Please input the correct email to reset the password.
</p>
<div class="grid grid-cols-1">
<FormKit label="Email" type="email" outer-class="text-left" />
<NuxtLink to="/reset-password">
<FormKit type="button" input-class="w-full">Validate Email</FormKit>
</NuxtLink>
</div>
<p class="mt-3 text-center text-slate-500">
Already have an account?
<NuxtLink to="/login" class="text-primary hover:underline"
>Login</NuxtLink
<FormKit type="form" :actions="false" @submit="changePassword">
<FormKit
type="email"
name="email"
placeholder="Sila masukkan emel anda"
validation="required|email"
:validation-messages="{
required: 'Emel wajib diisi',
email: 'Format emel tidak sah',
}"
>
</p>
<template #prefix>
<Icon
name="ph:envelope"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
</FormKit>
<rs-button @click="navigateTo('reset-password')" class="w-full mt-6">
<Icon name="ph:lock-fill" class="mr-2" />
Tukar kata laluan
</rs-button>
<div class="mt-4">
Kembali ke
<nuxt-link to="/login" class="text-sm text-blue-500">
log masuk
</nuxt-link>
</div>
</rs-card>
</div>
</div>
</template>
<style scoped>
/* Add any additional component-specific styles here */
</style>

117
pages/ikon/index.vue Normal file
View File

@ -0,0 +1,117 @@
<script setup>
import iconJson from "@/assets/json/iconamoon.json";
definePageMeta({
title: "Set Ikons",
middleware: ["auth"],
requiresAuth: true,
breadcrumb: [
{
name: "Set Ikons",
type: "current",
},
],
});
const copyToClipboard = (text) => {
let tagHtml = '<i class="icon-' + text + '"></i>';
navigator.clipboard.writeText(tagHtml);
};
const searchQuery = ref("");
const displayedIcons = ref([]);
const containerRef = ref(null);
const batchSize = 50;
let currentIndex = 0;
const filteredIcons = computed(() => {
if (!searchQuery.value) return iconJson;
return iconJson.filter((icon) =>
icon.name.toLowerCase().includes(searchQuery.value.toLowerCase())
);
});
const loadMoreIcons = () => {
const newIcons = filteredIcons.value.slice(
currentIndex,
currentIndex + batchSize
);
displayedIcons.value.push(...newIcons);
currentIndex += batchSize;
};
const resetIconDisplay = () => {
displayedIcons.value = [];
currentIndex = 0;
loadMoreIcons();
};
watch(searchQuery, () => {
resetIconDisplay();
nextTick(() => {
if (containerRef.value) {
containerRef.value.scrollTop = 0;
}
});
});
onMounted(() => {
resetIconDisplay();
});
const handleScroll = () => {
if (!containerRef.value) return;
const { scrollTop, scrollHeight, clientHeight } = containerRef.value;
if (scrollHeight - scrollTop <= clientHeight + 1) {
loadMoreIcons();
}
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-card>
<template #header>
Set Ikon Icomoon (Jumlah: {{ filteredIcons.length }})
</template>
<template #body>
<FormKit
type="text"
v-model="searchQuery"
placeholder="Search icons..."
class="mb-4"
/>
<div
ref="containerRef"
class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-8 max-h-[70vh] overflow-y-auto"
@scroll="handleScroll"
>
<div
v-for="icon in displayedIcons"
:key="icon.name"
class="flex items-center"
>
<Icon :name="icon.name" class="!w-10 !h-10 mr-4"></Icon>
<div class="flex flex-col">
<div class="text-sm">{{ icon.name }}</div>
<rs-button
@click="copyToClipboard(icon.name)"
class="!text-xs !py-1 !px-2 !mt-1"
>
<Icon name="ph:copy" class="!w-4 !h-4 mr-1"></Icon>
Copy
</rs-button>
</div>
</div>
</div>
<div v-if="displayedIcons.length === 0" class="text-center mt-4">
No icons found matching your search.
</div>
</template>
</rs-card>
</div>
</template>

View File

@ -0,0 +1,289 @@
<script setup>
definePageMeta({
title: "Butang",
breadcrumb: [
{
name: "Komponen",
type: "current",
},
],
});
const showCode1 = ref(false);
const showCode2 = ref(false);
const showCode3 = ref(false);
const showCode4 = ref(false);
const copyCode = (codeId) => {
const codeElement = document.getElementById(codeId);
if (codeElement) {
navigator.clipboard
.writeText(codeElement.textContent)
.then(() => {
// You can add a notification here if you want to inform the user that the code was copied
console.log("Code copied to clipboard");
})
.catch((err) => {
console.error("Failed to copy code: ", err);
});
}
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-card>
<template #header> Default </template>
<template #body>
<p class="mb-4">Use the <code>rs-button</code> to show badges.</p>
<div class="flex flex-wrap items-center justify-start gap-x-6">
<rs-button> Button </rs-button>
</div>
<div class="flex justify-end">
<button
class="text-sm border border-slate-200 py-1 px-3 rounded-lg my-2"
@click="showCode1 ? (showCode1 = false) : (showCode1 = true)"
>
Show Code
</button>
</div>
<ClientOnly>
<transition name="fade">
<div v-show="showCode1" v-highlight>
<div class="relative">
<button
@click="copyCode('code1')"
class="absolute top-4 right-2 text-sm bg-gray-300 hover:bg-gray-300 py-1 px-3 rounded z-10"
>
Copy
</button>
<NuxtScrollbar style="max-height: 300px">
<pre class="language-html shadow-none">
<code id="code1">
&lt;template&gt;
&lt;rs-button&gt;Button&lt;/rs-button&gt;
&lt;/template&gt;
&lt;script setup&gt;&lt;/script&gt;
</code>
</pre>
</NuxtScrollbar>
</div>
</div>
</transition>
</ClientOnly>
</template>
</rs-card>
<rs-card>
<template #header> Variant </template>
<template #body>
<p class="mb-4">
Button variant uses props <code>variant</code> to change the color of
the button. There are 6 variants: <code>primary</code>,
<code>info</code>, <code>success</code>, <code>warning</code> and
<code>danger</code>.
</p>
<div class="flex flex-wrap items-center justify-start gap-x-6">
<rs-button variant="primary"> Primary </rs-button>
<rs-button variant="secondary"> Secondary </rs-button>
<rs-button variant="info"> Info </rs-button>
<rs-button variant="success"> Success </rs-button>
<rs-button variant="warning"> Warning </rs-button>
<rs-button variant="danger"> Danger </rs-button>
</div>
<div class="flex justify-end">
<button
class="text-sm border border-slate-200 py-1 px-3 rounded-lg my-2"
@click="showCode2 ? (showCode2 = false) : (showCode2 = true)"
>
Show Code
</button>
</div>
<ClientOnly>
<transition name="fade">
<div v-show="showCode2" v-highlight>
<NuxtScrollbar style="max-height: 300px">
<pre class="language-html shadow-none">
<code>
&lt;template&gt;
&lt;rs-button variant="primary"&gt;Primary&lt;/rs-button&gt;
&lt;rs-button variant="secondary"&gt;Secondary&lt;/rs-button&gt;
&lt;rs-button variant="info"&gt;Info&lt;/rs-button&gt;
&lt;rs-button variant="success"&gt;Success&lt;/rs-button&gt;
&lt;rs-button variant="warning"&gt;Warning&lt;/rs-button&gt;
&lt;rs-button variant="danger"&gt;Danger&lt;/rs-button&gt;
&lt;/template&gt;
&lt;script setup&gt;&lt;/script&gt;
</code>
</pre>
</NuxtScrollbar>
</div>
</transition>
</ClientOnly>
</template>
</rs-card>
<rs-card>
<template #header> Variant Type </template>
<template #body>
<p class="mb-4">
Button variant type uses props <code>variant</code> to change the
shape of the button. There are 3 variant types: <code>fill</code>,
<code>outline</code> and <code>text</code>. <code>fill</code> is the
default. <code>outline</code> is used to create a button with a
border. <code>text</code> is used to create a button with no border
and no background.
</p>
<!-- Fill Button -->
<div class="my-6">
<div
class="font-semibold text-lg bg-[rgb(var(--bg-1))] w-full mb-4 pl-2"
>
Fill
</div>
<div class="flex flex-wrap items-center justify-start gap-x-6">
<rs-button variant="primary"> Primary </rs-button>
<rs-button variant="secondary"> Secondary </rs-button>
<rs-button variant="info"> Info </rs-button>
<rs-button variant="success"> Success </rs-button>
<rs-button variant="warning"> Warning </rs-button>
<rs-button variant="danger"> Danger </rs-button>
</div>
</div>
<!-- Outline Button -->
<div class="my-6">
<div
class="font-semibold text-lg bg-[rgb(var(--bg-1))] w-full mb-4 pl-2"
>
Outline
</div>
<div class="flex flex-wrap items-center justify-start gap-x-6">
<rs-button variant="primary-outline"> Primary </rs-button>
<rs-button variant="secondary-outline"> Secondary </rs-button>
<rs-button variant="info-outline"> Info </rs-button>
<rs-button variant="success-outline"> Success </rs-button>
<rs-button variant="warning-outline"> Warning </rs-button>
<rs-button variant="danger-outline"> Danger </rs-button>
</div>
</div>
<!-- Text Button -->
<div class="my-6">
<div
class="font-semibold text-lg bg-[rgb(var(--bg-1))] w-full mb-4 pl-2"
>
Text
</div>
<div class="flex flex-wrap items-center justify-start gap-x-6">
<rs-button variant="primary-text"> Primary </rs-button>
<rs-button variant="secondary-text"> Secondary </rs-button>
<rs-button variant="info-text"> Info </rs-button>
<rs-button variant="success-text"> Success </rs-button>
<rs-button variant="warning-text"> Warning </rs-button>
<rs-button variant="danger-text"> Danger </rs-button>
</div>
</div>
<div class="flex justify-end">
<button
class="text-sm border border-slate-200 py-1 px-3 rounded-lg my-2"
@click="showCode3 ? (showCode3 = false) : (showCode3 = true)"
>
Show Code
</button>
</div>
<ClientOnly>
<transition name="fade">
<div v-show="showCode3" v-highlight>
<NuxtScrollbar style="max-height: 300px">
<pre class="language-html shadow-none">
<code>
&lt;template&gt;
&lt;!-- Fill Button --&gt;
&lt;rs-button variant="primary"&gt;Primary&lt;/rs-button&gt;
&lt;rs-button variant="secondary"&gt;Secondary&lt;/rs-button&gt;
&lt;rs-button variant="info"&gt;Info&lt;/rs-button&gt;
&lt;rs-button variant="success"&gt;Success&lt;/rs-button&gt;
&lt;rs-button variant="warning"&gt;Warning&lt;/rs-button&gt;
&lt;rs-button variant="danger"&gt;Danger&lt;/rs-button&gt;
&lt;!-- Outline Button --&gt;
&lt;rs-button variant="primary-outline"&gt;Primary&lt;/rs-button&gt;
&lt;rs-button variant="secondary-outline"&gt;Secondary&lt;/rs-button&gt;
&lt;rs-button variant="info-outline"&gt;Info&lt;/rs-button&gt;
&lt;rs-button variant="success-outline"&gt;Success&lt;/rs-button&gt;
&lt;rs-button variant="warning-outline"&gt;Warning&lt;/rs-button&gt;
&lt;rs-button variant="danger-outline"&gt;Danger&lt;/rs-button&gt;
&lt;!-- Text Button --&gt;
&lt;rs-button variant="primary-text"&gt;Primary&lt;/rs-button&gt;
&lt;rs-button variant="secondary-text"&gt;Secondary&lt;/rs-button&gt;
&lt;rs-button variant="info-text"&gt;Info&lt;/rs-button&gt;
&lt;rs-button variant="success-text"&gt;Success&lt;/rs-button&gt;
&lt;rs-button variant="warning-text"&gt;Warning&lt;/rs-button&gt;
&lt;rs-button variant="danger-text"&gt;Danger&lt;/rs-button&gt;
&lt;/template&gt;
&lt;script setup&gt;&lt;/script&gt;
</code>
</pre>
</NuxtScrollbar>
</div>
</transition>
</ClientOnly>
</template>
</rs-card>
<rs-card>
<template #header> Size </template>
<template #body>
<p class="mb-4">
Button size uses props <code>size</code> to change the size of button.
There are 3 sizes: <code>small</code>, <code>medium</code> and
<code>large</code>.
</p>
<div class="flex flex-wrap items-center justify-start gap-x-6">
<rs-button size="sm"> Small </rs-button>
<rs-button size="md"> Medium </rs-button>
<rs-button size="lg"> Large </rs-button>
</div>
<div class="flex justify-end">
<button
class="text-sm border border-slate-200 py-1 px-3 rounded-lg my-2"
@click="showCode4 ? (showCode4 = false) : (showCode4 = true)"
>
Show Code
</button>
</div>
<ClientOnly>
<transition name="fade">
<div v-show="showCode4" v-highlight>
<NuxtScrollbar style="max-height: 300px">
<pre class="language-html shadow-none">
<code>
&lt;template&gt;
&lt;rs-button size="sm"&gt;Small&lt;/rs-button&gt;
&lt;rs-button size="md"&gt;Medium&lt;/rs-button&gt;
&lt;rs-button size="lg"&gt;Large&lt;/rs-button&gt;
&lt;/template&gt;
&lt;script setup&gt;&lt;/script&gt;
</code>
</pre>
</NuxtScrollbar>
</div>
</transition>
</ClientOnly>
</template>
</rs-card>
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup>
definePageMeta({
title: "Data Table",
breadcrumb: [
{
name: "Data Table",
type: "current",
},
],
});
</script>
<template>
<div>
<LayoutsBreadcrumb />
</div>
</template>

View File

@ -0,0 +1,17 @@
<script setup>
definePageMeta({
title: "Modal",
breadcrumb: [
{
name: "Modal",
type: "current",
},
],
});
</script>
<template>
<div>
<LayoutsBreadcrumb />
</div>
</template>

335
pages/laporan/index.vue Normal file
View File

@ -0,0 +1,335 @@
<script setup>
definePageMeta({
title: "Laporan",
breadcrumb: [
{
name: "Laporan",
type: "current",
},
],
});
const data = ref([
{
kriteriaPertama: "Aura",
kedua: "Hard",
ketiga: "Business Services Sales Representative",
keempat: "1969-04-19",
status: "Digantung",
tindakan: "...",
},
{
kriteriaPertama: "Chantal",
kedua: "Nailor",
ketiga: "Technical Services Librarian",
keempat: "1980-01-10",
status: "Tidak Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Cicely",
kedua: "Sigler",
ketiga: "Senior Research Officer",
keempat: "1960-03-15",
status: "Menunggu",
tindakan: "...",
},
{
kriteriaPertama: "Coy",
kedua: "Wollard",
ketiga: "Customer Service Operator",
keempat: "1982-10-12",
status: "Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Delma",
kedua: "Bonds",
ketiga: "Lead Brand Manager",
keempat: "1968-12-21",
status: "Menunggu",
tindakan: "...",
},
{
kriteriaPertama: "Lorin",
kedua: "Forbes",
ketiga: "Product Marketing Manager",
keempat: "1974-05-08",
status: "Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Eldon",
kedua: "Marcellus",
ketiga: "Software Developer",
keempat: "1987-11-30",
status: "Menunggu",
tindakan: "...",
},
{
kriteriaPertama: "Allie",
kedua: "Cordell",
ketiga: "UX Designer",
keempat: "1992-07-19",
status: "Digantung",
tindakan: "...",
},
{
kriteriaPertama: "Sheridan",
kedua: "Caldwell",
ketiga: "HR Manager",
keempat: "1970-03-22",
status: "Tidak Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Rowan",
kedua: "Douglas",
ketiga: "Financial Analyst",
keempat: "1983-08-13",
status: "Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Elvin",
kedua: "Temple",
ketiga: "Operations Manager",
keempat: "1976-01-11",
status: "Menunggu",
tindakan: "...",
},
{
kriteriaPertama: "Gisele",
kedua: "Erickson",
ketiga: "Public Relations Specialist",
keempat: "1989-09-30",
status: "Tidak Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Taryn",
kedua: "Ridgeway",
ketiga: "Content Strategist",
keempat: "1978-12-17",
status: "Digantung",
tindakan: "...",
},
{
kriteriaPertama: "Armand",
kedua: "Copeland",
ketiga: "Legal Advisor",
keempat: "1965-04-02",
status: "Menunggu",
tindakan: "...",
},
{
kriteriaPertama: "Tisha",
kedua: "Gillespie",
ketiga: "IT Support Specialist",
keempat: "1985-06-25",
status: "Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Brenton",
kedua: "Thornton",
ketiga: "Digital Marketing Specialist",
keempat: "1991-02-14",
status: "Digantung",
tindakan: "...",
},
{
kriteriaPertama: "Annis",
kedua: "Hobson",
ketiga: "Project Coordinator",
keempat: "1981-05-07",
status: "Menunggu",
tindakan: "...",
},
{
kriteriaPertama: "Dylan",
kedua: "Hudson",
ketiga: "Account Manager",
keempat: "1990-09-15",
status: "Tidak Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Kendall",
kedua: "Browning",
ketiga: "Supply Chain Manager",
keempat: "1973-07-03",
status: "Aktif",
tindakan: "...",
},
{
kriteriaPertama: "Maris",
kedua: "Woodard",
ketiga: "Customer Relations Specialist",
keempat: "1962-11-20",
status: "Menunggu",
tindakan: "...",
},
]);
const criteria = ref([
{
label: "Kriteria 1",
options: ["Aura", "Chantal", "Cicely", "Coy", "Delma"],
},
{
label: "Kriteria 2",
options: ["Hard", "Nailor", "Sigler", "Wollard", "Bonds"],
},
{
label: "Kriteria 3",
options: [
"Business Services Sales Representative",
"Technical Services Librarian",
"Senior Research Officer",
"Customer Service Operator",
"Lead Brand Manager",
],
},
{
label: "Kriteria 4",
options: [
"1969-04-19",
"1980-01-10",
"1960-03-15",
"1982-10-12",
"1968-12-21",
],
},
{
label: "Kriteria 5",
options: ["Digantung", "Tidak Aktif", "Menunggu", "Aktif"],
},
]);
const selectedCriteria = ref({
kriteria1: "",
kriteria2: "",
kriteria3: "",
kriteria4: "",
kriteria5: "",
});
const filteredData = ref([...data.value]);
const performSearch = () => {
filteredData.value = data.value.filter((item) => {
return (
(!selectedCriteria.value.kriteria1 ||
item.kriteriaPertama.includes(selectedCriteria.value.kriteria1)) &&
(!selectedCriteria.value.kriteria2 ||
item.kedua.includes(selectedCriteria.value.kriteria2)) &&
(!selectedCriteria.value.kriteria3 ||
item.ketiga.includes(selectedCriteria.value.kriteria3)) &&
(!selectedCriteria.value.kriteria4 ||
item.keempat.includes(selectedCriteria.value.kriteria4)) &&
(!selectedCriteria.value.kriteria5 ||
item.status.includes(selectedCriteria.value.kriteria5))
);
});
};
const resetFilter = () => {
selectedCriteria.value = {
kriteria1: "",
kriteria2: "",
kriteria3: "",
kriteria4: "",
kriteria5: "",
};
filteredData.value = [...data.value];
};
</script>
<template>
<div>
<LayoutsBreadcrumb />
<rs-card>
<template #header> Laporan</template>
<template #body>
<FormKit type="form" :actions="false">
<div class="grid grid-cols-2 gap-4 mb-4">
<FormKit
v-for="(item, index) in criteria.slice(0, 2)"
:key="index"
type="select"
:name="item.label.toLowerCase().replace(' ', '_')"
:label="item.label"
:options="item.options"
v-model="selectedCriteria[`kriteria${index + 1}`]"
placeholder="Pilih pilihan"
>
<template #label>
<label
class="formkit-label text-gray-700 dark:text-gray-200 block mb-2 font-semibold text-sm formkit-invalid:text-red-500"
>
{{ item.label }}
</label>
</template>
</FormKit>
</div>
<div class="grid grid-cols-3 gap-4">
<FormKit
v-for="(item, index) in criteria.slice(2)"
:key="index"
type="select"
:name="item.label.toLowerCase().replace(' ', '_')"
:label="item.label"
:options="item.options"
v-model="selectedCriteria[`kriteria${index + 3}`]"
placeholder="Pilih pilihan"
>
<template #label>
<label
class="formkit-label text-gray-700 dark:text-gray-200 block mb-2 font-semibold text-sm formkit-invalid:text-red-500"
>
{{ item.label }}
</label>
</template>
</FormKit>
</div>
<div class="flex justify-end items-center gap-2">
<rs-button @click="resetFilter" variant="primary-outline"
>Reset</rs-button
>
<rs-button type="submit" @click="performSearch">
Hantar Laporan
</rs-button>
</div>
</FormKit>
<rs-table
class="mt-8"
:key="filteredData"
:data="filteredData"
:pageSize="100"
>
<template v-slot:status="data">
<rs-badge
:variant="
data.text === 'Aktif'
? 'success'
: data.text === 'Menunggu'
? 'warning'
: data.text === 'Tidak Aktif'
? 'disabled'
: data.text === 'Digantung'
? 'danger'
: 'default'
"
>
{{ data.text }}
</rs-badge>
</template>
</rs-table>
</template>
</rs-card>
</div>
</template>

View File

@ -1,5 +1,6 @@
<script setup>
import { useUserStore } from "~/stores/user";
import { RecaptchaV2 } from "vue3-recaptcha-v2";
definePageMeta({
title: "Login",
@ -54,6 +55,19 @@ const login = async () => {
console.log(e);
}
};
const handleWidgetId = (widgetId) => {
console.log("Widget ID: ", widgetId);
};
const handleErrorCalback = () => {
console.log("Error callback");
};
const handleExpiredCallback = () => {
console.log("Expired callback");
};
const handleLoadCallback = (response) => {
console.log("Load callback", response);
};
</script>
<template>
@ -62,41 +76,53 @@ const login = async () => {
>
<div class="w-full md:w-3/4 lg:w-1/2 xl:w-2/6 relative">
<rs-card class="h-screen md:h-auto px-10 md:px-16 py-12 md:py-20 mb-0">
<div
class="absolute -bottom-3 left-3 img-container flex justify-start items-center mb-5"
>
<img
src="@/assets/img/logo/logo-word-black.svg"
class="max-w-[90px]"
/>
<div class="img-container flex justify-center items-center mb-5">
<img src="@/assets/img/logo/niise-logo.svg" class="max-w-[60px]" />
<img src="@/assets/img/logo/niise-text.svg" class="max-w-[120px]" />
</div>
<h3 class="mb-4">Login</h3>
<p class="text-slate-500 mb-6">
Welcome to Corrad. Please login to continue.
</p>
<p class="text-slate-500 text-lg mb-6">Log masuk ke akaun anda</p>
<div class="grid grid-cols-2">
<FormKit
type="text"
label="Username"
v-model="username"
validation="required"
placeholder="Masukkan ID Pengguna"
:classes="{
outer: 'col-span-2',
label: 'text-left',
messages: 'text-left',
}"
/>
<FormKit
:type="togglePasswordVisibility ? 'text' : 'password'"
label="Password"
v-model="password"
validation="required"
:classes="{
outer: 'col-span-2',
label: 'text-left',
messages: 'text-left',
:validation-messages="{
required: 'ID Pengguna wajib diisi.',
}"
>
<template #prefixIcon>
<Icon
name="ph:user-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
<FormKit
:type="togglePasswordVisibility ? 'text' : 'password'"
v-model="password"
validation="required"
placeholder="Masukkan Kata Laluan"
:classes="{
outer: 'col-span-2',
label: 'text-left',
messages: 'text-left',
}"
:validation-messages="{
required: 'Kata Laluan wajib diisi.',
}"
>
<template #prefixIcon>
<Icon
name="ph:lock-key-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
<template #suffix>
<div
class="bg-gray-100 hover:bg-slate-200 dark:bg-slate-700 hover:dark:bg-slate-900 h-full rounded-r-md p-3 flex justify-center items-center cursor-pointer"
@ -111,29 +137,43 @@ const login = async () => {
</div>
</template>
</FormKit>
<FormKit type="checkbox" label="Remember Me" />
<div class="col-span-2 mb-4">
<RecaptchaV2
@widget-id="handleWidgetId"
@error-callback="handleErrorCalback"
@expired-callback="handleExpiredCallback"
@load-callback="handleLoadCallback"
/>
</div>
<NuxtLink
class="flex items-center justify-end h-5 mt-1 text-primary hover:underline"
class="col-span-2 flex items-center justify-end h-5 mt-1 text-primary hover:underline mb-5"
to="forgot-password"
>Forgot Password?</NuxtLink
>
<!-- <NuxtLink to="/" class="col-span-2">
</NuxtLink> -->
Lupa Kata Laluan?
</NuxtLink>
<FormKit
type="button"
input-class="w-full"
outer-class="col-span-2"
@click="login"
>
Sign In
Log Masuk
<Icon name="ph:caret-circle-right" class="!w-5 !h-5 ml-1"></Icon>
</FormKit>
</div>
<p class="mt-3 text-center text-slate-500">
Don't have an account?
<NuxtLink to="/register" class="text-primary hover:underline"
>Sign Up</NuxtLink
>
</p>
<div class="flex justify-center items-center">
<hr class="w-full" />
<p class="w-full !text-gray-400">Tiada akaun?</p>
<hr class="w-full" />
</div>
<rs-button
@click="navigateTo('/register')"
class="w-full !bg-gray-100 !text-gray-600 border mt-5"
>
Daftar / Log masuk kali pertama
</rs-button>
</rs-card>
</div>
</div>

View File

@ -1,12 +1,34 @@
<script setup>
import { ref } from "vue";
import { RecaptchaV2 } from "vue3-recaptcha-v2";
definePageMeta({
title: "Register",
layout: "empty",
middleware: ["dashboard"],
});
const togglePasswordVisibility = ref(false);
const togglePasswordVisibility2 = ref(false);
const formData = ref({
fullName: "",
idNumber: "",
phoneNumber: "",
password: "",
confirmPassword: "",
email: "",
confirmEmail: "",
subscribeNewsletter: false,
agreeTerms: false,
});
const register = () => {
// Simulate registration without API call
console.log("Registration attempted with:", formData.value);
// Add your registration logic here
};
const handleRecaptcha = (response) => {
console.log("reCAPTCHA response:", response);
};
</script>
<template>
@ -15,81 +37,181 @@ const togglePasswordVisibility2 = ref(false);
>
<div class="w-full md:w-3/4 lg:w-1/2 xl:w-2/6 relative">
<rs-card class="h-screen md:h-auto px-10 md:px-16 py-12 md:py-20 mb-0">
<div
class="absolute -bottom-3 left-3 img-container flex justify-start items-center mb-5"
>
<img
src="@/assets/img/logo/logo-word-black.svg"
class="max-w-[90px]"
/>
<div class="text-center mb-8">
<div class="img-container flex justify-center items-center mb-5">
<img src="@/assets/img/logo/niise-logo.svg" class="max-w-[60px]" />
<img src="@/assets/img/logo/niise-text.svg" class="max-w-[120px]" />
</div>
<h2 class="mt-4 text-2xl font-bold text-gray-700">Daftar Akaun</h2>
<p class="text-sm text-gray-500">Semua medan adalah wajib</p>
</div>
<h3 class="mb-4">Sign Up</h3>
<p class="text-slate-500 mb-6 col-sp">
Please fill in the form to create an account.
</p>
<FormKit label="Username" type="text" label-class="text-left" />
<FormKit label="Email" type="email" label-class="text-left" />
<FormKit
:type="togglePasswordVisibility ? 'text' : 'password'"
label="Password"
type="password"
label-class="text-left"
>
<template #suffix>
<div
class="bg-gray-100 hover:bg-slate-200 dark:bg-slate-700 hover:dark:bg-slate-900 h-full rounded-r-md p-3 flex justify-center items-center cursor-pointer"
@click="togglePasswordVisibility = !togglePasswordVisibility"
>
<Icon
v-if="!togglePasswordVisibility"
name="ion:eye-outline"
size="19"
></Icon>
<Icon v-else name="ion:eye-off-outline" size="19"></Icon>
</div>
</template>
</FormKit>
<FormKit
:type="togglePasswordVisibility2 ? 'text' : 'password'"
label="Re-enter Password"
type="password"
label-class="text-left"
>
<template #suffix>
<div
class="bg-gray-100 hover:bg-slate-200 dark:bg-slate-700 hover:dark:bg-slate-900 h-full rounded-r-md p-3 flex justify-center items-center cursor-pointer"
@click="togglePasswordVisibility2 = !togglePasswordVisibility2"
>
<Icon
v-if="!togglePasswordVisibility2"
name="ion:eye-outline"
size="19"
></Icon>
<Icon v-else name="ion:eye-off-outline" size="19"></Icon>
</div>
</template>
</FormKit>
<FormKit
type="checkbox"
label="agreement"
outer-class="col-span-1 md:col-span-2"
>
<template #label
>I agree to the
<a class="text-primary hover:underline ml-1">Term and Services</a>
</template>
</FormKit>
<NuxtLink to="/" class="col-span-1 md:col-span-2">
<FormKit type="button" input-class="w-full">Sign up</FormKit>
</NuxtLink>
<p class="mt-3 text-center text-slate-500">
Already have an account?
<NuxtLink to="/login" class="text-primary hover:underline"
>Login</NuxtLink
<FormKit type="form" :actions="false" @submit="register">
<FormKit
type="text"
name="fullName"
placeholder="Nama Penuh"
validation="required"
:validation-messages="{
required: 'Nama Penuh wajib diisi',
}"
>
</p>
<template #prefixIcon>
<Icon
name="ph:user-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
<div class="grid grid-cols-1 md:grid-cols-2 gap-0 md:gap-4">
<FormKit
type="text"
name="idNumber"
placeholder="Nombor Mykad / Nombor Pasport"
validation="required"
:validation-messages="{
required: 'Nombor Mykad / Nombor Pasport wajib diisi',
}"
>
<template #prefixIcon>
<Icon
name="ph:identification-card-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
<FormKit
type="tel"
name="phoneNumber"
placeholder="Nombor Telefon"
validation="required"
:validation-messages="{
required: 'Nombor Telefon wajib diisi',
}"
>
<template #prefixIcon>
<Icon
name="ph:device-mobile-camera-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-0 md:gap-4">
<FormKit
type="password"
name="password"
placeholder="Kata Laluan"
validation="required"
:validation-messages="{
required: 'Kata Laluan wajib diisi',
}"
>
<template #prefixIcon>
<Icon
name="ph:lock-key-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
<FormKit
type="password"
name="confirmPassword"
placeholder="Pengesahan Kata Laluan"
validation="required|confirm"
:validation-messages="{
required: 'Pengesahan Kata Laluan wajib diisi',
confirm: 'Kata Laluan tidak sepadan',
}"
:validation-rules="{
confirm: (value) => value === value.password,
}"
>
<template #prefixIcon>
<Icon
name="ph:lock-key-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-0 md:gap-4">
<FormKit
type="email"
name="email"
placeholder="Emel"
validation="required|email"
:validation-messages="{
required: 'Emel wajib diisi',
email: 'Format emel tidak sah',
}"
>
<template #prefixIcon>
<Icon
name="ph:envelope-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
<FormKit
type="email"
name="confirmEmail"
placeholder="Pengesahan Emel"
validation="required|confirm"
:validation-messages="{
required: 'Pengesahan Emel wajib diisi',
confirm: 'Emel tidak sepadan',
}"
:validation-rules="{
confirm: (value) => value === value.email,
}"
>
<template #prefixIcon>
<Icon
name="ph:envelope-fill"
class="!w-5 !h-5 ml-3 text-gray-500"
></Icon>
</template>
</FormKit>
</div>
<div class="flex justify-start mb-4 mt-2">
<RecaptchaV2 @verify="handleRecaptcha" />
</div>
<FormKit
type="checkbox"
name="subscribeNewsletter"
label="Melanggan ke newsletter bulanan"
/>
<FormKit
type="checkbox"
name="agreeTerms"
label="Setuju dengan terma perkhidmatan"
validation="accepted"
:validation-messages="{
accepted: 'Anda mesti bersetuju dengan terma perkhidmatan',
}"
>
<template #label>
Setuju dengan
<a href="#" class="text-blue-600 ml-1">terma perkhidmatan</a>
</template>
</FormKit>
<rs-button btn-type="submit" class="w-full"> Daftar Akaun </rs-button>
</FormKit>
<div class="text-center mt-4">
<p class="text-sm text-gray-500">
Sudah mempunyai akaun?
<nuxt-link to="/login" class="text-blue-600">Log Masuk</nuxt-link>
</p>
</div>
</rs-card>
</div>
</div>

View File

@ -1,42 +0,0 @@
<script setup>
definePageMeta({
title: "Reset Password",
layout: "empty",
middleware: ["dashboard"],
});
</script>
<template>
<div
class="flex-none md:flex justify-center text-center items-center h-screen"
>
<div class="w-full md:w-3/4 lg:w-1/2 xl:w-2/6 relative">
<rs-card class="h-screen md:h-auto px-10 md:px-16 py-12 md:py-20 mb-0">
<div
class="absolute -bottom-3 left-3 img-container flex justify-start items-center mb-5"
>
<img
src="@/assets/img/logo/logo-word-black.svg"
class="max-w-[90px]"
/>
</div>
<h3 class="mb-4">Reset Password</h3>
<p class="text-slate-500 mb-6">
Please fill in the form to reset your password.
</p>
<div class="grid grid-cols-1">
<FormKit label="Email" type="email" outer-class="text-left" />
<FormKit label="Password" type="password" outer-class="text-left" />
<FormKit
label="Re-enter Password"
type="password"
outer-class="text-left"
/>
<NuxtLink to="/login">
<FormKit type="button" input-class="w-full">Reset Password</FormKit>
</NuxtLink>
</div>
</rs-card>
</div>
</div>
</template>

View File

@ -0,0 +1,8 @@
import { install } from "vue3-recaptcha-v2";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(install, {
sitekey: "6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI",
cnDomains: false,
});
});

View File

@ -46,17 +46,19 @@ model role {
}
model lookup {
lookupID Int @id @default(autoincrement())
lookupOrder Int?
lookupTitle String? @db.VarChar(255)
lookupRefCode String? @db.VarChar(255)
lookupValue String? @db.VarChar(255)
lookupType String? @db.VarChar(255)
lookupStatus String? @db.VarChar(255)
lookupCreatedDate DateTime? @db.DateTime(0)
lookupModifiedDate DateTime? @db.DateTime(0)
assistant assistant[]
project project[]
lookupID Int @id @default(autoincrement())
lookupOrder Int?
lookupTitle String? @db.VarChar(255)
lookupRefCode String? @db.VarChar(255)
lookupValue String? @db.VarChar(255)
lookupType String? @db.VarChar(255)
lookupStatus String? @db.VarChar(255)
lookupCreatedDate DateTime? @db.DateTime(0)
lookupModifiedDate DateTime? @db.DateTime(0)
assistant assistant[]
project project[]
project_project_projectViewTypeTolookup project[] @relation("project_projectViewTypeTolookup")
thread thread[]
}
model userrole {
@ -152,17 +154,21 @@ model project {
projectDescription String? @db.VarChar(255)
projectDefault Boolean? @default(dbgenerated("b'0'")) @db.Bit(1)
projectType Int?
projectPublic Boolean? @db.Bit(1)
projectViewType Int?
projectCreatedDate DateTime? @db.DateTime(0)
projectModifiedDate DateTime? @db.DateTime(0)
chat chat[]
user user? @relation(fields: [userID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "project_ibfk_2")
lookup lookup? @relation(fields: [projectType], references: [lookupID], onDelete: NoAction, onUpdate: NoAction, map: "project_ibfk_3")
lookup_project_projectViewTypeTolookup lookup? @relation("project_projectViewTypeTolookup", fields: [projectViewType], references: [lookupID], onDelete: NoAction, onUpdate: NoAction, map: "project_ibfk_4")
project_permission_project_permission_projectPermissionIDToproject project_permission? @relation("project_permission_projectPermissionIDToproject")
project_permission_project_permission_projectIDToproject project_permission[] @relation("project_permission_projectIDToproject")
repository repository[]
@@index([projectType], map: "projectType")
@@index([userID], map: "userID")
@@index([projectViewType], map: "projectViewType")
}
model project_permission {
@ -205,12 +211,15 @@ model thread {
assistantID Int?
threadTitle String? @db.VarChar(255)
threadOAIID String? @unique(map: "threadOAIID") @db.VarChar(255)
threadStatus Int?
threadCreatedDate DateTime? @db.DateTime(0)
threadModifiedDate DateTime? @db.DateTime(0)
chat chat[]
assistant assistant? @relation(fields: [assistantID], references: [assistantID], onDelete: NoAction, onUpdate: NoAction, map: "thread_ibfk_1")
user user? @relation(fields: [userID], references: [userID], onDelete: NoAction, onUpdate: NoAction, map: "thread_ibfk_2")
lookup lookup? @relation(fields: [threadStatus], references: [lookupID], onDelete: NoAction, onUpdate: NoAction, map: "thread_ibfk_3")
@@index([assistantID], map: "assistantID")
@@index([userID], map: "userID")
@@index([threadStatus], map: "threadStatus")
}

View File

@ -9227,6 +9227,11 @@ vue3-perfect-scrollbar@^1.6.1:
perfect-scrollbar "^1.5.5"
postcss-import "^12.0.0"
vue3-recaptcha-v2@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/vue3-recaptcha-v2/-/vue3-recaptcha-v2-2.0.2.tgz#127d568d90cd3de3588429eb500d16f408380f13"
integrity sha512-uIxJG4+BVquSVJxCLdkJV3Y0IgXPG+sWqAG34qwHt9MungDFshuKprtrN48oJHcLXePLey/FEo2B5pbfMB5cjQ==
vue@^2.5.16:
version "2.7.14"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.14.tgz#3743dcd248fd3a34d421ae456b864a0246bafb17"