Autosave: 20260618-225950
This commit is contained in:
parent
a4ecdd89c9
commit
0925ce59ce
@ -3,10 +3,9 @@ import { mdiLogout, mdiClose } from '@mdi/js'
|
|||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
import AsideMenuList from './AsideMenuList'
|
import AsideMenuList from './AsideMenuList'
|
||||||
import { MenuAsideItem } from '../interfaces'
|
import { MenuAsideItem } from '../interfaces'
|
||||||
import { useAppSelector } from '../stores/hooks'
|
import { useAppDispatch, useAppSelector } from '../stores/hooks'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
|
||||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@ -68,7 +67,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
|
|||||||
>
|
>
|
||||||
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
<div className="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
||||||
|
|
||||||
<b className="font-black">Vorta Universe MVP</b>
|
<b className="font-black">VORTA-COMMERCE</b>
|
||||||
|
|
||||||
|
|
||||||
{organizationName && <p>{organizationName}</p>}
|
{organizationName && <p>{organizationName}</p>}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React, {useEffect, useRef} from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
|
||||||
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
|
||||||
import BaseDivider from './BaseDivider'
|
import BaseDivider from './BaseDivider'
|
||||||
import BaseIcon from './BaseIcon'
|
import BaseIcon from './BaseIcon'
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
|
|||||||
|
|
||||||
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
|
||||||
|
|
||||||
export const appTitle = 'created by Flatlogic generator!'
|
export const appTitle = 'Ekosistem Kepercayaan Digital'
|
||||||
|
|
||||||
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import React, { ReactNode, useEffect } from 'react'
|
import React, { ReactNode, useEffect, useState } from 'react'
|
||||||
import { useState } from 'react'
|
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
|
||||||
import menuAside from '../menuAside'
|
import menuAside from '../menuAside'
|
||||||
|
|||||||
@ -1,355 +1,487 @@
|
|||||||
import * as icon from '@mdi/js';
|
import * as icon from '@mdi/js';
|
||||||
import { MenuAsideItem } from './interfaces'
|
import { MenuAsideItem } from './interfaces';
|
||||||
|
|
||||||
const menuAside: MenuAsideItem[] = [
|
const menuAside: MenuAsideItem[] = [
|
||||||
{
|
{
|
||||||
href: '/dashboard',
|
href: '/vorta-commerce',
|
||||||
icon: icon.mdiViewDashboardOutline,
|
icon:
|
||||||
label: 'Dashboard',
|
'mdiStorefrontOutline' in icon
|
||||||
|
? (icon['mdiStorefrontOutline' as keyof typeof icon] as string)
|
||||||
|
: icon.mdiViewDashboardOutline,
|
||||||
|
label: 'VORTA-Commerce',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
href: '/mega-super-app',
|
||||||
|
label: 'Mega Super-App',
|
||||||
|
icon:
|
||||||
|
'mdiApps' in icon
|
||||||
|
? (icon['mdiApps' as keyof typeof icon] as string)
|
||||||
|
: icon.mdiViewDashboardOutline,
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
href: '/vorta-synapse',
|
||||||
|
label: '04 Vorta Synapse',
|
||||||
|
icon:
|
||||||
|
'mdiTransitConnectionVariant' in icon
|
||||||
|
? (icon['mdiTransitConnectionVariant' as keyof typeof icon] as string)
|
||||||
|
: icon.mdiViewDashboardOutline,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
label: 'Users',
|
label: 'Pengguna',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
icon: icon.mdiAccountGroup ?? icon.mdiTable,
|
||||||
permissions: 'READ_USERS'
|
permissions: 'READ_USERS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/roles/roles-list',
|
href: '/roles/roles-list',
|
||||||
label: 'Roles',
|
label: 'Peran',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
|
||||||
permissions: 'READ_ROLES'
|
permissions: 'READ_ROLES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/permissions/permissions-list',
|
href: '/permissions/permissions-list',
|
||||||
label: 'Permissions',
|
label: 'Izin Akses',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
|
||||||
permissions: 'READ_PERMISSIONS'
|
permissions: 'READ_PERMISSIONS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/organizations/organizations-list',
|
href: '/organizations/organizations-list',
|
||||||
label: 'Organizations',
|
label: 'Organisasi',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: icon.mdiTable ?? icon.mdiTable,
|
icon: icon.mdiTable ?? icon.mdiTable,
|
||||||
permissions: 'READ_ORGANIZATIONS'
|
permissions: 'READ_ORGANIZATIONS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/trust_profiles/trust_profiles-list',
|
href: '/trust_profiles/trust_profiles-list',
|
||||||
label: 'Trust profiles',
|
label: 'Profil Kepercayaan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiShieldCheck' in icon ? icon['mdiShieldCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_TRUST_PROFILES'
|
'mdiShieldCheck' in icon
|
||||||
|
? icon['mdiShieldCheck' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_TRUST_PROFILES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/identity_verifications/identity_verifications-list',
|
href: '/identity_verifications/identity_verifications-list',
|
||||||
label: 'Identity verifications',
|
label: 'Verifikasi Identitas',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCardAccountDetailsOutline' in icon ? icon['mdiCardAccountDetailsOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_IDENTITY_VERIFICATIONS'
|
'mdiCardAccountDetailsOutline' in icon
|
||||||
|
? icon['mdiCardAccountDetailsOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_IDENTITY_VERIFICATIONS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/wallets/wallets-list',
|
href: '/wallets/wallets-list',
|
||||||
label: 'Wallets',
|
label: 'Dompet',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiWallet' in icon ? icon['mdiWallet' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_WALLETS'
|
'mdiWallet' in icon
|
||||||
|
? icon['mdiWallet' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_WALLETS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/wallet_transactions/wallet_transactions-list',
|
href: '/wallet_transactions/wallet_transactions-list',
|
||||||
label: 'Wallet transactions',
|
label: 'Transaksi Dompet',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_WALLET_TRANSACTIONS'
|
'mdiSwapHorizontal' in icon
|
||||||
|
? icon['mdiSwapHorizontal' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_WALLET_TRANSACTIONS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/chats/chats-list',
|
href: '/chats/chats-list',
|
||||||
label: 'Chats',
|
label: 'Percakapan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiChat' in icon ? icon['mdiChat' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_CHATS'
|
'mdiChat' in icon
|
||||||
|
? icon['mdiChat' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_CHATS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/chat_participants/chat_participants-list',
|
href: '/chat_participants/chat_participants-list',
|
||||||
label: 'Chat participants',
|
label: 'Peserta Percakapan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_CHAT_PARTICIPANTS'
|
'mdiAccountGroup' in icon
|
||||||
|
? icon['mdiAccountGroup' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_CHAT_PARTICIPANTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/messages/messages-list',
|
href: '/messages/messages-list',
|
||||||
label: 'Messages',
|
label: 'Pesan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_MESSAGES'
|
'mdiMessageTextOutline' in icon
|
||||||
|
? icon['mdiMessageTextOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_MESSAGES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/stores/stores-list',
|
href: '/stores/stores-list',
|
||||||
label: 'Stores',
|
label: 'Toko',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiStore' in icon ? icon['mdiStore' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_STORES'
|
'mdiStore' in icon
|
||||||
|
? icon['mdiStore' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_STORES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/addresses/addresses-list',
|
href: '/addresses/addresses-list',
|
||||||
label: 'Addresses',
|
label: 'Alamat',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_ADDRESSES'
|
'mdiMapMarker' in icon
|
||||||
|
? icon['mdiMapMarker' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_ADDRESSES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/product_categories/product_categories-list',
|
href: '/product_categories/product_categories-list',
|
||||||
label: 'Product categories',
|
label: 'Kategori Produk',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiShapeOutline' in icon ? icon['mdiShapeOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_PRODUCT_CATEGORIES'
|
'mdiShapeOutline' in icon
|
||||||
|
? icon['mdiShapeOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_PRODUCT_CATEGORIES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/products/products-list',
|
href: '/products/products-list',
|
||||||
label: 'Products',
|
label: 'Produk',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiPackageVariantClosed' in icon ? icon['mdiPackageVariantClosed' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_PRODUCTS'
|
'mdiPackageVariantClosed' in icon
|
||||||
|
? icon['mdiPackageVariantClosed' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_PRODUCTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/orders/orders-list',
|
href: '/orders/orders-list',
|
||||||
label: 'Orders',
|
label: 'Pesanan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiReceiptTextOutline' in icon ? icon['mdiReceiptTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_ORDERS'
|
'mdiReceiptTextOutline' in icon
|
||||||
|
? icon['mdiReceiptTextOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_ORDERS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/order_items/order_items-list',
|
href: '/order_items/order_items-list',
|
||||||
label: 'Order items',
|
label: 'Item Pesanan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_ORDER_ITEMS'
|
'mdiFormatListBulleted' in icon
|
||||||
|
? icon['mdiFormatListBulleted' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_ORDER_ITEMS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/payments/payments-list',
|
href: '/payments/payments-list',
|
||||||
label: 'Payments',
|
label: 'Pembayaran',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCreditCardOutline' in icon ? icon['mdiCreditCardOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_PAYMENTS'
|
'mdiCreditCardOutline' in icon
|
||||||
|
? icon['mdiCreditCardOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_PAYMENTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/product_reviews/product_reviews-list',
|
href: '/product_reviews/product_reviews-list',
|
||||||
label: 'Product reviews',
|
label: 'Ulasan Produk',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiStarCircleOutline' in icon ? icon['mdiStarCircleOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_PRODUCT_REVIEWS'
|
'mdiStarCircleOutline' in icon
|
||||||
|
? icon['mdiStarCircleOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_PRODUCT_REVIEWS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/shipments/shipments-list',
|
href: '/shipments/shipments-list',
|
||||||
label: 'Shipments',
|
label: 'Pengiriman',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiTruckDeliveryOutline' in icon ? icon['mdiTruckDeliveryOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_SHIPMENTS'
|
'mdiTruckDeliveryOutline' in icon
|
||||||
|
? icon['mdiTruckDeliveryOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_SHIPMENTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/posts/posts-list',
|
href: '/posts/posts-list',
|
||||||
label: 'Posts',
|
label: 'Postingan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiPlayCircleOutline' in icon ? icon['mdiPlayCircleOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_POSTS'
|
'mdiPlayCircleOutline' in icon
|
||||||
|
? icon['mdiPlayCircleOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_POSTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/deep_dive_links/deep_dive_links-list',
|
href: '/deep_dive_links/deep_dive_links-list',
|
||||||
label: 'Deep dive links',
|
label: 'Tautan Deep Dive',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_DEEP_DIVE_LINKS'
|
'mdiLinkVariant' in icon
|
||||||
|
? icon['mdiLinkVariant' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_DEEP_DIVE_LINKS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/citations/citations-list',
|
href: '/citations/citations-list',
|
||||||
label: 'Citations',
|
label: 'Sitasi',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiFileDocumentCheckOutline' in icon ? icon['mdiFileDocumentCheckOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_CITATIONS'
|
'mdiFileDocumentCheckOutline' in icon
|
||||||
|
? icon['mdiFileDocumentCheckOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_CITATIONS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/post_engagements/post_engagements-list',
|
href: '/post_engagements/post_engagements-list',
|
||||||
label: 'Post engagements',
|
label: 'Interaksi Postingan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiThumbUpOutline' in icon ? icon['mdiThumbUpOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_POST_ENGAGEMENTS'
|
'mdiThumbUpOutline' in icon
|
||||||
|
? icon['mdiThumbUpOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_POST_ENGAGEMENTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/rewards/rewards-list',
|
href: '/rewards/rewards-list',
|
||||||
label: 'Rewards',
|
label: 'Hadiah',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiTrophyOutline' in icon ? icon['mdiTrophyOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_REWARDS'
|
'mdiTrophyOutline' in icon
|
||||||
|
? icon['mdiTrophyOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_REWARDS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/forum_spaces/forum_spaces-list',
|
href: '/forum_spaces/forum_spaces-list',
|
||||||
label: 'Forum spaces',
|
label: 'Ruang Forum',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiForumOutline' in icon ? icon['mdiForumOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_FORUM_SPACES'
|
'mdiForumOutline' in icon
|
||||||
|
? icon['mdiForumOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_FORUM_SPACES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/forum_threads/forum_threads-list',
|
href: '/forum_threads/forum_threads-list',
|
||||||
label: 'Forum threads',
|
label: 'Topik Forum',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiMessageBulleted' in icon ? icon['mdiMessageBulleted' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_FORUM_THREADS'
|
'mdiMessageBulleted' in icon
|
||||||
|
? icon['mdiMessageBulleted' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_FORUM_THREADS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/forum_posts/forum_posts-list',
|
href: '/forum_posts/forum_posts-list',
|
||||||
label: 'Forum posts',
|
label: 'Posting Forum',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiReplyOutline' in icon ? icon['mdiReplyOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_FORUM_POSTS'
|
'mdiReplyOutline' in icon
|
||||||
|
? icon['mdiReplyOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_FORUM_POSTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/job_companies/job_companies-list',
|
href: '/job_companies/job_companies-list',
|
||||||
label: 'Job companies',
|
label: 'Perusahaan Lowongan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiOfficeBuildingOutline' in icon ? icon['mdiOfficeBuildingOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_JOB_COMPANIES'
|
'mdiOfficeBuildingOutline' in icon
|
||||||
|
? icon['mdiOfficeBuildingOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_JOB_COMPANIES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/job_listings/job_listings-list',
|
href: '/job_listings/job_listings-list',
|
||||||
label: 'Job listings',
|
label: 'Daftar Lowongan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiBriefcaseOutline' in icon ? icon['mdiBriefcaseOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_JOB_LISTINGS'
|
'mdiBriefcaseOutline' in icon
|
||||||
|
? icon['mdiBriefcaseOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_JOB_LISTINGS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/job_applications/job_applications-list',
|
href: '/job_applications/job_applications-list',
|
||||||
label: 'Job applications',
|
label: 'Lamaran Kerja',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiFileAccountOutline' in icon ? icon['mdiFileAccountOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_JOB_APPLICATIONS'
|
'mdiFileAccountOutline' in icon
|
||||||
|
? icon['mdiFileAccountOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_JOB_APPLICATIONS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/user_credentials/user_credentials-list',
|
href: '/user_credentials/user_credentials-list',
|
||||||
label: 'User credentials',
|
label: 'Kredensial Pengguna',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCertificateOutline' in icon ? icon['mdiCertificateOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_USER_CREDENTIALS'
|
'mdiCertificateOutline' in icon
|
||||||
|
? icon['mdiCertificateOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_USER_CREDENTIALS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/affiliate_programs/affiliate_programs-list',
|
href: '/affiliate_programs/affiliate_programs-list',
|
||||||
label: 'Affiliate programs',
|
label: 'Program Afiliasi',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiAccountNetworkOutline' in icon ? icon['mdiAccountNetworkOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_AFFILIATE_PROGRAMS'
|
'mdiAccountNetworkOutline' in icon
|
||||||
|
? icon['mdiAccountNetworkOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_AFFILIATE_PROGRAMS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/affiliate_memberships/affiliate_memberships-list',
|
href: '/affiliate_memberships/affiliate_memberships-list',
|
||||||
label: 'Affiliate memberships',
|
label: 'Keanggotaan Afiliasi',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiLinkBoxOutline' in icon ? icon['mdiLinkBoxOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_AFFILIATE_MEMBERSHIPS'
|
'mdiLinkBoxOutline' in icon
|
||||||
|
? icon['mdiLinkBoxOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_AFFILIATE_MEMBERSHIPS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/commissions/commissions-list',
|
href: '/commissions/commissions-list',
|
||||||
label: 'Commissions',
|
label: 'Komisi',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCashCheck' in icon ? icon['mdiCashCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_COMMISSIONS'
|
'mdiCashCheck' in icon
|
||||||
|
? icon['mdiCashCheck' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_COMMISSIONS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/geo_rules/geo_rules-list',
|
href: '/geo_rules/geo_rules-list',
|
||||||
label: 'Geo rules',
|
label: 'Aturan Geo',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiMapMarkerRadiusOutline' in icon ? icon['mdiMapMarkerRadiusOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_GEO_RULES'
|
'mdiMapMarkerRadiusOutline' in icon
|
||||||
|
? icon['mdiMapMarkerRadiusOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_GEO_RULES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/facta_queries/facta_queries-list',
|
href: '/facta_queries/facta_queries-list',
|
||||||
label: 'Facta queries',
|
label: 'Kueri Facta',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiMagnify' in icon ? icon['mdiMagnify' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_FACTA_QUERIES'
|
'mdiMagnify' in icon
|
||||||
|
? icon['mdiMagnify' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_FACTA_QUERIES',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/facta_answers/facta_answers-list',
|
href: '/facta_answers/facta_answers-list',
|
||||||
label: 'Facta answers',
|
label: 'Jawaban Facta',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiShieldSearchOutline' in icon ? icon['mdiShieldSearchOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_FACTA_ANSWERS'
|
'mdiShieldSearchOutline' in icon
|
||||||
|
? icon['mdiShieldSearchOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_FACTA_ANSWERS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/subscriptions/subscriptions-list',
|
href: '/subscriptions/subscriptions-list',
|
||||||
label: 'Subscriptions',
|
label: 'Langganan',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiCrownOutline' in icon ? icon['mdiCrownOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_SUBSCRIPTIONS'
|
'mdiCrownOutline' in icon
|
||||||
|
? icon['mdiCrownOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_SUBSCRIPTIONS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/api_clients/api_clients-list',
|
href: '/api_clients/api_clients-list',
|
||||||
label: 'Api clients',
|
label: 'Klien API',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiApi' in icon ? icon['mdiApi' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_API_CLIENTS'
|
'mdiApi' in icon
|
||||||
|
? icon['mdiApi' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_API_CLIENTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/audit_events/audit_events-list',
|
href: '/audit_events/audit_events-list',
|
||||||
label: 'Audit events',
|
label: 'Peristiwa Audit',
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
icon: 'mdiClipboardTextOutline' in icon ? icon['mdiClipboardTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
|
icon:
|
||||||
permissions: 'READ_AUDIT_EVENTS'
|
'mdiClipboardTextOutline' in icon
|
||||||
|
? icon['mdiClipboardTextOutline' as keyof typeof icon]
|
||||||
|
: (icon.mdiTable ?? icon.mdiTable),
|
||||||
|
permissions: 'READ_AUDIT_EVENTS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
label: 'Profile',
|
label: 'Profil',
|
||||||
icon: icon.mdiAccountCircle,
|
icon: icon.mdiAccountCircle,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
{
|
{
|
||||||
href: '/api-docs',
|
href: '/api-docs',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
label: 'Swagger API',
|
label: 'Dokumentasi API',
|
||||||
icon: icon.mdiFileCode,
|
icon: icon.mdiFileCode,
|
||||||
permissions: 'READ_API_DOCS'
|
permissions: 'READ_API_DOCS',
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export default menuAside
|
export default menuAside;
|
||||||
|
|||||||
@ -10,8 +10,8 @@ import {
|
|||||||
mdiThemeLightDark,
|
mdiThemeLightDark,
|
||||||
mdiGithub,
|
mdiGithub,
|
||||||
mdiVuejs,
|
mdiVuejs,
|
||||||
} from '@mdi/js'
|
} from '@mdi/js';
|
||||||
import { MenuNavBarItem } from './interfaces'
|
import { MenuNavBarItem } from './interfaces';
|
||||||
|
|
||||||
const menuNavBar: MenuNavBarItem[] = [
|
const menuNavBar: MenuNavBarItem[] = [
|
||||||
{
|
{
|
||||||
@ -19,7 +19,7 @@ const menuNavBar: MenuNavBarItem[] = [
|
|||||||
menu: [
|
menu: [
|
||||||
{
|
{
|
||||||
icon: mdiAccount,
|
icon: mdiAccount,
|
||||||
label: 'My Profile',
|
label: 'Profil Saya',
|
||||||
href: '/profile',
|
href: '/profile',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -27,27 +27,25 @@ const menuNavBar: MenuNavBarItem[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiLogout,
|
icon: mdiLogout,
|
||||||
label: 'Log Out',
|
label: 'Keluar',
|
||||||
isLogout: true,
|
isLogout: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiThemeLightDark,
|
icon: mdiThemeLightDark,
|
||||||
label: 'Light/Dark',
|
label: 'Terang/Gelap',
|
||||||
isDesktopNoLabel: true,
|
isDesktopNoLabel: true,
|
||||||
isToggleLightDark: true,
|
isToggleLightDark: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: mdiLogout,
|
icon: mdiLogout,
|
||||||
label: 'Log out',
|
label: 'Keluar',
|
||||||
isDesktopNoLabel: true,
|
isDesktopNoLabel: true,
|
||||||
isLogout: true,
|
isLogout: true,
|
||||||
},
|
},
|
||||||
]
|
|
||||||
|
|
||||||
export const webPagesNavBar = [
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default menuNavBar
|
export const webPagesNavBar = [];
|
||||||
|
|
||||||
|
export default menuNavBar;
|
||||||
|
|||||||
@ -121,7 +121,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
setSteps(loginSteps);
|
setSteps(loginSteps);
|
||||||
setStepName('loginSteps');
|
setStepName('loginSteps');
|
||||||
setStepsEnabled(true);
|
setStepsEnabled(true);
|
||||||
}else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
|
}else if (router.pathname === '/vorta-commerce' && !isCompleted('appSteps')) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSteps(appSteps);
|
setSteps(appSteps);
|
||||||
setStepName('appSteps');
|
setStepName('appSteps');
|
||||||
@ -149,8 +149,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
|||||||
setStepsEnabled(false);
|
setStepsEnabled(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const title = 'Vorta Universe MVP'
|
const title = 'VORTA-COMMERCE'
|
||||||
const description = "Unified super-app for verified identity, chat-to-commerce, and citation-backed search with a single wallet."
|
const description = "Commerce operating system for products, inventory, orders, payments, shipping, marketing, reports, and settings."
|
||||||
const url = "https://flatlogic.com/"
|
const url = "https://flatlogic.com/"
|
||||||
const image = "https://project-screens.s3.amazonaws.com/screenshots/40285/app-hero-20260618-210659.png"
|
const image = "https://project-screens.s3.amazonaws.com/screenshots/40285/app-hero-20260618-210659.png"
|
||||||
const imageWidth = '1920'
|
const imageWidth = '1920'
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@ export default function Error() {
|
|||||||
<SectionFullScreen bg="pinkRed">
|
<SectionFullScreen bg="pinkRed">
|
||||||
<CardBox
|
<CardBox
|
||||||
className="w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12 shadow-2xl"
|
className="w-11/12 md:w-7/12 lg:w-6/12 xl:w-4/12 shadow-2xl"
|
||||||
footer={<BaseButton href="/dashboard" label="Done" color="danger" />}
|
footer={<BaseButton href="/vorta-commerce" label="Done" color="danger" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h1 className="text-2xl">Unhandled exception</h1>
|
<h1 className="text-2xl">Unhandled exception</h1>
|
||||||
|
|||||||
@ -1,166 +1,830 @@
|
|||||||
|
import React, {
|
||||||
import React, { useEffect, useState } from 'react';
|
FormEvent,
|
||||||
import type { ReactElement } from 'react';
|
ReactElement,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import BaseButton from '../components/BaseButton';
|
import BaseButton from '../components/BaseButton';
|
||||||
import CardBox from '../components/CardBox';
|
import CardBox from '../components/CardBox';
|
||||||
import SectionFullScreen from '../components/SectionFullScreen';
|
|
||||||
import LayoutGuest from '../layouts/Guest';
|
import LayoutGuest from '../layouts/Guest';
|
||||||
import BaseDivider from '../components/BaseDivider';
|
|
||||||
import BaseButtons from '../components/BaseButtons';
|
|
||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
import { useAppSelector } from '../stores/hooks';
|
|
||||||
import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
|
|
||||||
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
|
|
||||||
|
|
||||||
|
type TrustForm = {
|
||||||
|
profileName: string;
|
||||||
|
profileType: 'creator' | 'umkm' | 'company';
|
||||||
|
verifiedDocuments: string;
|
||||||
|
hasBiometric: boolean;
|
||||||
|
hasTaxId: boolean;
|
||||||
|
hasTransactionHistory: boolean;
|
||||||
|
citedSources: string;
|
||||||
|
verifiedReviews: boolean;
|
||||||
|
localPresence: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Starter() {
|
type TrustReport = TrustForm & {
|
||||||
const [illustrationImage, setIllustrationImage] = useState({
|
id: string;
|
||||||
src: undefined,
|
score: number;
|
||||||
photographer: undefined,
|
tier: string;
|
||||||
photographer_url: undefined,
|
createdAt: string;
|
||||||
})
|
recommendations: string[];
|
||||||
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
|
};
|
||||||
const [contentType, setContentType] = useState('image');
|
|
||||||
const [contentPosition, setContentPosition] = useState('background');
|
|
||||||
const textColor = useAppSelector((state) => state.style.linkColor);
|
|
||||||
|
|
||||||
const title = 'Vorta Universe MVP'
|
const defaultForm: TrustForm = {
|
||||||
|
profileName: '',
|
||||||
|
profileType: 'umkm',
|
||||||
|
verifiedDocuments: '2',
|
||||||
|
hasBiometric: true,
|
||||||
|
hasTaxId: false,
|
||||||
|
hasTransactionHistory: true,
|
||||||
|
citedSources: '1',
|
||||||
|
verifiedReviews: false,
|
||||||
|
localPresence: true,
|
||||||
|
};
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
const storageKey = 'vorta-trust-reports';
|
||||||
useEffect(() => {
|
|
||||||
async function fetchData() {
|
|
||||||
const image = await getPexelsImage();
|
|
||||||
const video = await getPexelsVideo();
|
|
||||||
setIllustrationImage(image);
|
|
||||||
setIllustrationVideo(video);
|
|
||||||
}
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const imageBlock = (image) => (
|
const profileTypeLabels: Record<TrustForm['profileType'], string> = {
|
||||||
<div
|
creator: 'Kreator',
|
||||||
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
umkm: 'UMKM',
|
||||||
style={{
|
company: 'Perusahaan',
|
||||||
backgroundImage: `${
|
};
|
||||||
image
|
|
||||||
? `url(${image?.src?.original})`
|
const getTier = (score: number) => {
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
if (score >= 86) return 'Nexus Prime';
|
||||||
}`,
|
if (score >= 70) return 'Pertumbuhan Terverifikasi';
|
||||||
backgroundSize: 'cover',
|
if (score >= 31) return 'Layak Pulse';
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
return 'Perlu Verifikasi';
|
||||||
}}
|
};
|
||||||
>
|
|
||||||
<div className='flex justify-center w-full bg-blue-300/20'>
|
const clampNumber = (value: string, min: number, max: number) => {
|
||||||
<a
|
const parsed = Number.parseInt(value, 10);
|
||||||
className='text-[8px]'
|
|
||||||
href={image?.photographer_url}
|
if (Number.isNaN(parsed)) return min;
|
||||||
target='_blank'
|
|
||||||
rel='noreferrer'
|
return Math.min(Math.max(parsed, min), max);
|
||||||
>
|
};
|
||||||
Photo by {image?.photographer} on Pexels
|
|
||||||
</a>
|
const buildRecommendations = (form: TrustForm, score: number) => {
|
||||||
</div>
|
const recommendations = [];
|
||||||
</div>
|
const documents = clampNumber(form.verifiedDocuments, 0, 4);
|
||||||
|
const sources = clampNumber(form.citedSources, 0, 5);
|
||||||
|
|
||||||
|
if (!form.hasBiometric)
|
||||||
|
recommendations.push(
|
||||||
|
'Aktifkan pencocokan wajah untuk mengikat akun dengan manusia asli.',
|
||||||
|
);
|
||||||
|
if (!form.hasTaxId)
|
||||||
|
recommendations.push(
|
||||||
|
'Tambahkan NPWP atau legalitas bisnis untuk transaksi bernilai tinggi.',
|
||||||
|
);
|
||||||
|
if (documents < 3)
|
||||||
|
recommendations.push(
|
||||||
|
'Unggah minimal tiga dokumen valid agar skor Nexus naik lebih cepat.',
|
||||||
|
);
|
||||||
|
if (sources < 3)
|
||||||
|
recommendations.push(
|
||||||
|
'Tambahkan Deep Dive Link primer agar konten lolos cek sitasi Facta.',
|
||||||
|
);
|
||||||
|
if (!form.verifiedReviews)
|
||||||
|
recommendations.push(
|
||||||
|
'Kunci ulasan hanya dari pembeli terverifikasi untuk ulasan bebas manipulasi.',
|
||||||
|
);
|
||||||
|
if (score < 31)
|
||||||
|
recommendations.push(
|
||||||
|
'Prioritaskan verifikasi dasar agar dapat menulis di Vorta Pulse.',
|
||||||
);
|
);
|
||||||
|
|
||||||
const videoBlock = (video) => {
|
return recommendations.length
|
||||||
if (video?.video_files?.length > 0) {
|
? recommendations
|
||||||
return (
|
: ['Profil siap diprioritaskan untuk visibilitas organik lintas VORTA.'];
|
||||||
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
|
};
|
||||||
<video
|
|
||||||
className='absolute top-0 left-0 w-full h-full object-cover'
|
const calculateReport = (form: TrustForm): TrustReport => {
|
||||||
autoPlay
|
const documents = clampNumber(form.verifiedDocuments, 0, 4);
|
||||||
loop
|
const sources = clampNumber(form.citedSources, 0, 5);
|
||||||
muted
|
const score = Math.min(
|
||||||
>
|
100,
|
||||||
<source src={video?.video_files[0]?.link} type='video/mp4'/>
|
14 +
|
||||||
Your browser does not support the video tag.
|
documents * 9 +
|
||||||
</video>
|
(form.hasBiometric ? 18 : 0) +
|
||||||
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
|
(form.hasTaxId ? 12 : 0) +
|
||||||
<a
|
(form.hasTransactionHistory ? 14 : 0) +
|
||||||
className='text-[8px]'
|
sources * 4 +
|
||||||
href={video?.user?.url}
|
(form.verifiedReviews ? 10 : 0) +
|
||||||
target='_blank'
|
(form.localPresence ? 6 : 0),
|
||||||
rel='noreferrer'
|
);
|
||||||
>
|
|
||||||
Video by {video.user.name} on Pexels
|
return {
|
||||||
</a>
|
...form,
|
||||||
</div>
|
verifiedDocuments: String(documents),
|
||||||
</div>)
|
citedSources: String(sources),
|
||||||
}
|
id: `${Date.now()}`,
|
||||||
|
score,
|
||||||
|
tier: getTier(score),
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
recommendations: buildRecommendations(form, score),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const scoreGradient = (score: number) => {
|
||||||
|
if (score >= 70) return 'from-emerald-400 to-cyan-300';
|
||||||
|
if (score >= 31) return 'from-amber-300 to-orange-400';
|
||||||
|
|
||||||
|
return 'from-rose-400 to-red-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollToSection = (sectionId: string) => {
|
||||||
|
const element = document.getElementById(sectionId);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function VortaLanding() {
|
||||||
|
const [form, setForm] = useState<TrustForm>(defaultForm);
|
||||||
|
const [reports, setReports] = useState<TrustReport[]>([]);
|
||||||
|
const [activeReportId, setActiveReportId] = useState<string>('');
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
const stored = window.localStorage.getItem(storageKey);
|
||||||
|
const parsedReports = stored ? (JSON.parse(stored) as TrustReport[]) : [];
|
||||||
|
|
||||||
|
if (Array.isArray(parsedReports)) {
|
||||||
|
const normalizedReports = parsedReports.map((report) => ({
|
||||||
|
...report,
|
||||||
|
tier: getTier(report.score),
|
||||||
|
recommendations: buildRecommendations(report, report.score),
|
||||||
|
}));
|
||||||
|
|
||||||
|
setReports(normalizedReports);
|
||||||
|
setActiveReportId(normalizedReports[0]?.id || '');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'Failed to read VORTA trust reports from localStorage:',
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
window.localStorage.removeItem(storageKey);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.localStorage.setItem(storageKey, JSON.stringify(reports));
|
||||||
|
}, [reports]);
|
||||||
|
|
||||||
|
const activeReport = useMemo(
|
||||||
|
() => reports.find((report) => report.id === activeReportId) || reports[0],
|
||||||
|
[activeReportId, reports],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateField = (field: keyof TrustForm, value: string | boolean) => {
|
||||||
|
setForm((current) => ({ ...current, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveReport = (nextForm: TrustForm) => {
|
||||||
|
const report = calculateReport(nextForm);
|
||||||
|
|
||||||
|
setReports((current) => [report, ...current].slice(0, 6));
|
||||||
|
setActiveReportId(report.id);
|
||||||
|
window.setTimeout(() => scrollToSection('reports'), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const trimmedName = form.profileName.trim();
|
||||||
|
|
||||||
|
if (trimmedName.length < 3) {
|
||||||
|
setErrorMessage(
|
||||||
|
'Nama profil minimal 3 karakter agar laporan dapat dibuat.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setErrorMessage('');
|
||||||
|
saveReport({ ...form, profileName: trimmedName });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSampleReport = () => {
|
||||||
|
const sampleForm: TrustForm = {
|
||||||
|
profileName: 'Warung Nusantara Bandung',
|
||||||
|
profileType: 'umkm',
|
||||||
|
verifiedDocuments: '4',
|
||||||
|
hasBiometric: true,
|
||||||
|
hasTaxId: true,
|
||||||
|
hasTransactionHistory: true,
|
||||||
|
citedSources: '4',
|
||||||
|
verifiedReviews: true,
|
||||||
|
localPresence: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setForm(sampleForm);
|
||||||
|
setErrorMessage('');
|
||||||
|
saveReport(sampleForm);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
setForm(defaultForm);
|
||||||
|
setErrorMessage('');
|
||||||
|
scrollToSection('trust-flow');
|
||||||
|
};
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ label: 'kanal utama', value: '4 mode' },
|
||||||
|
{ label: 'ruang kerja', value: '1 app' },
|
||||||
|
{ label: 'akses ringan', value: '<3 detik' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const pillars = [
|
||||||
|
[
|
||||||
|
'Mega Super-App',
|
||||||
|
'Chat bisnis, sosial, marketplace, dan dompet dalam satu ruang kerja ringan.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Vorta Nexus',
|
||||||
|
'Skor kepercayaan 0-100 untuk identitas, bisnis, ulasan, dan akses komunitas.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Facta.AI',
|
||||||
|
'Riset bebas SEO spam dengan Truth Score dan peta rujukan langsung.',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'Vorta Synapse',
|
||||||
|
'Protokol distribusi, afiliasi, dan logistik pintar untuk ekonomi kreator.',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<>
|
||||||
style={
|
|
||||||
contentPosition === 'background'
|
|
||||||
? {
|
|
||||||
backgroundImage: `${
|
|
||||||
illustrationImage
|
|
||||||
? `url(${illustrationImage.src?.original})`
|
|
||||||
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
|
|
||||||
}`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'left center',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}
|
|
||||||
: {}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Head>
|
<Head>
|
||||||
<title>{getPageTitle('Starter Page')}</title>
|
<title>{getPageTitle('01 Mega Super-App')}</title>
|
||||||
|
<meta
|
||||||
|
name='description'
|
||||||
|
content='01 Mega Super-App VORTA menyatukan chat bisnis, sosial, marketplace, dan dompet dalam satu ruang kerja ringan.'
|
||||||
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<SectionFullScreen bg='violet'>
|
<main className='min-h-screen overflow-hidden bg-[#07111f] text-white'>
|
||||||
<div
|
<section className='relative isolate px-5 py-5 sm:px-8 lg:px-12'>
|
||||||
className={`flex ${
|
<div className='absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top_left,#1de9b6_0,transparent_30%),radial-gradient(circle_at_80%_20%,#6c63ff_0,transparent_28%),linear-gradient(135deg,#07111f_0%,#101b34_45%,#07111f_100%)] opacity-95' />
|
||||||
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
|
<div className='absolute left-1/2 top-20 -z-10 h-72 w-72 -translate-x-1/2 rounded-full bg-cyan-400/20 blur-3xl' />
|
||||||
} min-h-screen w-full`}
|
|
||||||
>
|
<nav className='mx-auto flex max-w-7xl items-center justify-between rounded-full border border-white/10 bg-white/[0.08] px-4 py-3 shadow-2xl shadow-cyan-950/30 backdrop-blur-xl'>
|
||||||
{contentType === 'image' && contentPosition !== 'background'
|
<Link
|
||||||
? imageBlock(illustrationImage)
|
href='/'
|
||||||
: null}
|
className='flex items-center gap-3 font-semibold tracking-wide'
|
||||||
{contentType === 'video' && contentPosition !== 'background'
|
>
|
||||||
? videoBlock(illustrationVideo)
|
<span className='grid h-10 w-10 place-items-center rounded-2xl bg-gradient-to-br from-[#1DE9B6] to-[#6C63FF] text-lg font-black text-[#07111f]'>
|
||||||
: null}
|
V
|
||||||
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
|
</span>
|
||||||
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
|
<span>VORTA UNIVERSE</span>
|
||||||
<CardBoxComponentTitle title="Welcome to your Vorta Universe MVP app!"/>
|
</Link>
|
||||||
|
<div className='hidden items-center gap-2 text-sm text-slate-300 md:flex'>
|
||||||
<div className="space-y-3">
|
{[
|
||||||
<p className='text-center '>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
|
['pillars', 'Super-App'],
|
||||||
<p className='text-center '>For guides and documentation please check
|
['trust-flow', 'Skor'],
|
||||||
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
|
['reports', 'Laporan'],
|
||||||
|
].map(([sectionId, label]) => (
|
||||||
|
<button
|
||||||
|
key={sectionId}
|
||||||
|
type='button'
|
||||||
|
onClick={() => scrollToSection(sectionId)}
|
||||||
|
className='rounded-full px-4 py-2 transition hover:bg-white/10 hover:text-white focus:outline-none focus:ring-2 focus:ring-cyan-300/50'
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<BaseButton
|
||||||
<BaseButtons>
|
href='/login'
|
||||||
|
label='Masuk Admin'
|
||||||
|
color='info'
|
||||||
|
roundedFull
|
||||||
|
className='border-cyan-300/40 bg-cyan-300 text-slate-950 hover:bg-white'
|
||||||
|
/>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className='mx-auto mt-4 flex max-w-7xl flex-wrap justify-center gap-2 text-sm text-slate-200 md:hidden'>
|
||||||
|
{[
|
||||||
|
['pillars', 'Super-App'],
|
||||||
|
['trust-flow', 'Skor'],
|
||||||
|
['reports', 'Laporan'],
|
||||||
|
].map(([sectionId, label]) => (
|
||||||
|
<button
|
||||||
|
key={sectionId}
|
||||||
|
type='button'
|
||||||
|
onClick={() => scrollToSection(sectionId)}
|
||||||
|
className='rounded-full border border-white/10 bg-white/[0.08] px-4 py-2 transition hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-cyan-300/50'
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mx-auto grid max-w-7xl gap-10 py-20 lg:grid-cols-[1.05fr_0.95fr] lg:items-center lg:py-28'>
|
||||||
|
<div>
|
||||||
|
<div className='mb-6 inline-flex rounded-full border border-emerald-300/30 bg-emerald-300/10 px-4 py-2 text-sm font-medium text-emerald-100'>
|
||||||
|
Fokus pertama: 01 Mega Super-App
|
||||||
|
</div>
|
||||||
|
<h1 className='max-w-4xl text-5xl font-black leading-tight tracking-tight text-white md:text-7xl'>
|
||||||
|
Chat bisnis, sosial, marketplace, dan dompet dalam satu ruang
|
||||||
|
kerja ringan.
|
||||||
|
</h1>
|
||||||
|
<p className='mt-6 max-w-2xl text-lg leading-8 text-slate-300'>
|
||||||
|
Mulai dari percakapan tim, feed komunitas, etalase produk,
|
||||||
|
pembayaran, sampai riwayat transaksi—semuanya berjalan di satu
|
||||||
|
workspace cepat dengan identitas dan skor kepercayaan bawaan.
|
||||||
|
</p>
|
||||||
|
<div className='mt-9 flex flex-col gap-3 sm:flex-row'>
|
||||||
<BaseButton
|
<BaseButton
|
||||||
href='/login'
|
onClick={() => scrollToSection('pillars')}
|
||||||
label='Login'
|
label='Lihat Alur Super-App'
|
||||||
color='info'
|
color='success'
|
||||||
className='w-full'
|
roundedFull
|
||||||
|
className='border-emerald-300 bg-[#1DE9B6] px-7 py-3 text-slate-950 hover:bg-white'
|
||||||
/>
|
/>
|
||||||
|
<BaseButton
|
||||||
|
onClick={() => scrollToSection('trust-flow')}
|
||||||
|
label='Simulasikan Kepercayaan'
|
||||||
|
color='white'
|
||||||
|
outline
|
||||||
|
roundedFull
|
||||||
|
className='border-white/30 bg-white/5 px-7 py-3 text-white hover:bg-white hover:text-slate-950'
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
href='/login'
|
||||||
|
label='Masuk ke Admin'
|
||||||
|
color='white'
|
||||||
|
outline
|
||||||
|
roundedFull
|
||||||
|
className='border-white/30 bg-white/5 px-7 py-3 text-white hover:bg-white hover:text-slate-950'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='mt-10 grid max-w-xl grid-cols-3 gap-3'>
|
||||||
|
{stats.map((stat) => (
|
||||||
|
<div
|
||||||
|
key={stat.label}
|
||||||
|
className='rounded-3xl border border-white/10 bg-white/[0.08] p-4 backdrop-blur'
|
||||||
|
>
|
||||||
|
<div className='text-2xl font-black text-[#1DE9B6]'>
|
||||||
|
{stat.value}
|
||||||
|
</div>
|
||||||
|
<div className='mt-1 text-xs uppercase tracking-[0.22em] text-slate-400'>
|
||||||
|
{stat.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</BaseButtons>
|
<div className='relative'>
|
||||||
</CardBox>
|
<div className='absolute -inset-4 rounded-[2rem] bg-gradient-to-br from-cyan-300/30 via-indigo-400/20 to-emerald-300/20 blur-2xl' />
|
||||||
</div>
|
<CardBox
|
||||||
</div>
|
className='relative border-white/10 bg-white/10 text-white shadow-2xl shadow-black/30 backdrop-blur-2xl'
|
||||||
</SectionFullScreen>
|
cardBoxClassName='p-0'
|
||||||
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
>
|
||||||
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
|
<div className='rounded-t-2xl border-b border-white/10 bg-slate-950/60 px-6 py-4'>
|
||||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
<div className='flex items-center justify-between'>
|
||||||
Privacy Policy
|
<div>
|
||||||
</Link>
|
<p className='text-sm text-slate-400'>
|
||||||
</div>
|
Pratinjau ruang kerja langsung
|
||||||
|
</p>
|
||||||
|
<h2 className='text-xl font-bold'>
|
||||||
|
Mega Super-App Workspace
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span className='rounded-full bg-emerald-300/15 px-3 py-1 text-xs font-semibold text-emerald-200'>
|
||||||
|
AKTIF
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-5 p-6'>
|
||||||
|
<div className='grid grid-cols-2 gap-3'>
|
||||||
|
{[
|
||||||
|
'Chat bisnis',
|
||||||
|
'Feed sosial',
|
||||||
|
'Marketplace',
|
||||||
|
'Dompet digital',
|
||||||
|
].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item}
|
||||||
|
className='rounded-2xl border border-white/10 bg-white/[0.08] p-4'
|
||||||
|
>
|
||||||
|
<div className='mb-3 h-2 w-12 rounded-full bg-gradient-to-r from-[#1DE9B6] to-[#6C63FF]' />
|
||||||
|
<p className='text-sm font-semibold'>{item}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className='rounded-3xl border border-cyan-200/20 bg-cyan-200/10 p-5'>
|
||||||
|
<div className='flex items-end justify-between'>
|
||||||
|
<div>
|
||||||
|
<p className='text-sm text-cyan-100/80'>
|
||||||
|
Aktivitas terpadu hari ini
|
||||||
|
</p>
|
||||||
|
<p className='text-5xl font-black text-cyan-100'>128</p>
|
||||||
|
</div>
|
||||||
|
<p className='rounded-full bg-white px-3 py-1 text-sm font-bold text-slate-950'>
|
||||||
|
Transaksi & pesan sinkron
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='mt-5 h-3 rounded-full bg-slate-900/70'>
|
||||||
|
<div className='h-3 w-[92%] rounded-full bg-gradient-to-r from-[#1DE9B6] to-[#6C63FF]' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
</div>
|
<section
|
||||||
|
id='pillars'
|
||||||
|
className='border-y border-white/10 bg-white/[0.03] px-5 py-16 sm:px-8 lg:px-12'
|
||||||
|
>
|
||||||
|
<div className='mx-auto max-w-7xl'>
|
||||||
|
<div className='mb-8 max-w-2xl'>
|
||||||
|
<p className='text-sm font-semibold uppercase tracking-[0.28em] text-[#1DE9B6]'>
|
||||||
|
Fokus pertama: 01 Mega Super-App
|
||||||
|
</p>
|
||||||
|
<h2 className='mt-3 text-3xl font-black md:text-4xl'>
|
||||||
|
Satu ruang kerja ringan yang menghubungkan percakapan,
|
||||||
|
komunitas, perdagangan, pembayaran, dan reputasi pengguna.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
|
||||||
|
{pillars.map(([title, description], index) => (
|
||||||
|
<article
|
||||||
|
key={title}
|
||||||
|
className={`group rounded-[1.75rem] border p-6 transition hover:-translate-y-1 hover:border-cyan-200/40 hover:bg-white/10 ${
|
||||||
|
index === 0
|
||||||
|
? 'border-cyan-200/50 bg-cyan-200/10 shadow-2xl shadow-cyan-950/30'
|
||||||
|
: 'border-white/10 bg-slate-950/40'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className='mb-8 flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-[#1DE9B6] to-[#6C63FF] text-lg font-black text-slate-950'>
|
||||||
|
0{index + 1}
|
||||||
|
</div>
|
||||||
|
<h3 className='text-xl font-bold'>{title}</h3>
|
||||||
|
<p className='mt-3 text-sm leading-6 text-slate-300'>
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id='trust-flow' className='px-5 py-20 sm:px-8 lg:px-12'>
|
||||||
|
<div className='mx-auto grid max-w-7xl gap-8 lg:grid-cols-[0.9fr_1.1fr]'>
|
||||||
|
<div className='lg:sticky lg:top-8 lg:self-start'>
|
||||||
|
<p className='text-sm font-semibold uppercase tracking-[0.28em] text-[#1DE9B6]'>
|
||||||
|
Mesin kepercayaan bawaan
|
||||||
|
</p>
|
||||||
|
<h2 className='mt-3 text-4xl font-black'>
|
||||||
|
Skor Kepercayaan untuk Super-App
|
||||||
|
</h2>
|
||||||
|
<p className='mt-4 text-slate-300'>
|
||||||
|
Setiap akun, toko, percakapan, ulasan, dan transaksi bisa
|
||||||
|
diberi sinyal kepercayaan agar marketplace dan komunitas tetap
|
||||||
|
aman tanpa terasa berat bagi pengguna.
|
||||||
|
</p>
|
||||||
|
<div className='mt-6 rounded-3xl border border-white/10 bg-white/[0.08] p-5 text-sm leading-6 text-slate-300'>
|
||||||
|
<strong className='text-white'>Aturan validasi:</strong> nama
|
||||||
|
profil wajib minimal 3 karakter. Skor 31+ menandakan profil
|
||||||
|
layak masuk feed sosial, chat bisnis, dan marketplace publik.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CardBox
|
||||||
|
className='border-white/10 bg-white text-slate-950 shadow-2xl shadow-cyan-950/20'
|
||||||
|
cardBoxClassName='p-0'
|
||||||
|
>
|
||||||
|
<form onSubmit={handleSubmit} className='p-6 md:p-8'>
|
||||||
|
<div className='grid gap-5 md:grid-cols-2'>
|
||||||
|
<label className='md:col-span-2'>
|
||||||
|
<span className='mb-2 block text-sm font-bold text-slate-700'>
|
||||||
|
Nama profil
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
value={form.profileName}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateField('profileName', event.target.value)
|
||||||
|
}
|
||||||
|
className='w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none transition focus:border-cyan-400 focus:ring-4 focus:ring-cyan-100'
|
||||||
|
placeholder='Contoh: Warung Nusantara Bandung'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span className='mb-2 block text-sm font-bold text-slate-700'>
|
||||||
|
Tipe profil
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={form.profileType}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateField(
|
||||||
|
'profileType',
|
||||||
|
event.target.value as TrustForm['profileType'],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none transition focus:border-cyan-400 focus:ring-4 focus:ring-cyan-100'
|
||||||
|
>
|
||||||
|
<option value='creator'>Kreator / edukator</option>
|
||||||
|
<option value='umkm'>UMKM / penjual lokal</option>
|
||||||
|
<option value='company'>Perusahaan / rekruter</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span className='mb-2 block text-sm font-bold text-slate-700'>
|
||||||
|
Dokumen terverifikasi
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
max='4'
|
||||||
|
value={form.verifiedDocuments}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateField('verifiedDocuments', event.target.value)
|
||||||
|
}
|
||||||
|
className='w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none transition focus:border-cyan-400 focus:ring-4 focus:ring-cyan-100'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span className='mb-2 block text-sm font-bold text-slate-700'>
|
||||||
|
Deep Dive Links valid
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
max='5'
|
||||||
|
value={form.citedSources}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateField('citedSources', event.target.value)
|
||||||
|
}
|
||||||
|
className='w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 outline-none transition focus:border-cyan-400 focus:ring-4 focus:ring-cyan-100'
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 grid gap-3 md:grid-cols-2'>
|
||||||
|
{[
|
||||||
|
['hasBiometric', 'Pencocokan wajah biometrik aktif'],
|
||||||
|
['hasTaxId', 'NPWP / legalitas bisnis valid'],
|
||||||
|
['hasTransactionHistory', 'Riwayat transaksi bersih'],
|
||||||
|
['verifiedReviews', 'Ulasan dikunci oleh bukti bayar'],
|
||||||
|
['localPresence', 'Lokasi nyata / geofence aktif'],
|
||||||
|
].map(([field, label]) => (
|
||||||
|
<label
|
||||||
|
key={field}
|
||||||
|
className='flex items-center gap-3 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm font-semibold text-slate-700'
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type='checkbox'
|
||||||
|
checked={Boolean(form[field as keyof TrustForm])}
|
||||||
|
onChange={(event) =>
|
||||||
|
updateField(
|
||||||
|
field as keyof TrustForm,
|
||||||
|
event.target.checked,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='h-5 w-5 rounded border-slate-300 text-cyan-500 focus:ring-cyan-400'
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{errorMessage ? (
|
||||||
|
<div className='mt-5 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm font-semibold text-rose-700'>
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className='mt-7 flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between'>
|
||||||
|
<p className='text-sm text-slate-500'>
|
||||||
|
Laporan tersimpan di browser ini sebagai riwayat demo MVP.
|
||||||
|
</p>
|
||||||
|
<div className='flex flex-col gap-2 sm:flex-row'>
|
||||||
|
<BaseButton
|
||||||
|
onClick={handleSampleReport}
|
||||||
|
label='Buat Contoh Cepat'
|
||||||
|
color='white'
|
||||||
|
outline
|
||||||
|
roundedFull
|
||||||
|
className='border-slate-300 px-5 py-3 text-slate-700 hover:bg-slate-100'
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
onClick={handleResetForm}
|
||||||
|
label='Reset Formulir'
|
||||||
|
color='white'
|
||||||
|
outline
|
||||||
|
roundedFull
|
||||||
|
className='border-slate-300 px-5 py-3 text-slate-700 hover:bg-slate-100'
|
||||||
|
/>
|
||||||
|
<BaseButton
|
||||||
|
type='submit'
|
||||||
|
label='Buat Laporan Kepercayaan'
|
||||||
|
color='info'
|
||||||
|
roundedFull
|
||||||
|
className='bg-slate-950 px-6 py-3 text-white hover:bg-cyan-600'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id='reports' className='px-5 pb-20 sm:px-8 lg:px-12'>
|
||||||
|
<div className='mx-auto grid max-w-7xl gap-8 lg:grid-cols-[0.9fr_1.1fr]'>
|
||||||
|
<CardBox
|
||||||
|
className='border-white/10 bg-white/[0.08] text-white backdrop-blur'
|
||||||
|
cardBoxClassName='p-0'
|
||||||
|
>
|
||||||
|
<div className='border-b border-white/10 p-6'>
|
||||||
|
<h2 className='text-2xl font-black'>
|
||||||
|
Daftar Laporan Kepercayaan
|
||||||
|
</h2>
|
||||||
|
<p className='mt-2 text-sm text-slate-300'>
|
||||||
|
Pilih laporan untuk membuka detail simulasi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='space-y-3 p-4'>
|
||||||
|
{reports.length ? (
|
||||||
|
reports.map((report) => (
|
||||||
|
<button
|
||||||
|
key={report.id}
|
||||||
|
type='button'
|
||||||
|
onClick={() => setActiveReportId(report.id)}
|
||||||
|
className={`w-full rounded-3xl border p-4 text-left transition focus:outline-none focus:ring-4 focus:ring-cyan-300/30 ${activeReport?.id === report.id ? 'border-cyan-300 bg-cyan-300/15' : 'border-white/10 bg-slate-950/30 hover:border-white/30 hover:bg-white/10'}`}
|
||||||
|
>
|
||||||
|
<div className='flex items-start justify-between gap-3'>
|
||||||
|
<div>
|
||||||
|
<p className='font-bold'>{report.profileName}</p>
|
||||||
|
<p className='mt-1 text-xs uppercase tracking-[0.18em] text-slate-400'>
|
||||||
|
{profileTypeLabels[report.profileType]} ·{' '}
|
||||||
|
{new Date(report.createdAt).toLocaleDateString(
|
||||||
|
'id-ID',
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span className='rounded-full bg-white px-3 py-1 text-sm font-black text-slate-950'>
|
||||||
|
{report.score}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className='rounded-3xl border border-dashed border-white/20 p-8 text-center text-slate-300'>
|
||||||
|
<p>
|
||||||
|
Belum ada laporan. Isi simulator di atas untuk membuat
|
||||||
|
Laporan Kepercayaan pertama.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
onClick={handleSampleReport}
|
||||||
|
className='mt-5 rounded-full bg-cyan-300 px-5 py-2 text-sm font-bold text-slate-950 transition hover:bg-white focus:outline-none focus:ring-4 focus:ring-cyan-300/30'
|
||||||
|
>
|
||||||
|
Buat Contoh Laporan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox
|
||||||
|
className='border-white/10 bg-white text-slate-950 shadow-2xl shadow-black/20'
|
||||||
|
cardBoxClassName='p-0'
|
||||||
|
>
|
||||||
|
{activeReport ? (
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
className={`rounded-t-2xl bg-gradient-to-br ${scoreGradient(activeReport.score)} p-8 text-slate-950`}
|
||||||
|
>
|
||||||
|
<p className='text-sm font-black uppercase tracking-[0.25em] opacity-70'>
|
||||||
|
Detail Laporan Kepercayaan
|
||||||
|
</p>
|
||||||
|
<div className='mt-6 flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between'>
|
||||||
|
<div>
|
||||||
|
<h2 className='text-3xl font-black'>
|
||||||
|
{activeReport.profileName}
|
||||||
|
</h2>
|
||||||
|
<p className='mt-2 font-semibold'>
|
||||||
|
{getTier(activeReport.score)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className='text-7xl font-black leading-none'>
|
||||||
|
{activeReport.score}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='mt-7 h-3 rounded-full bg-slate-950/25'>
|
||||||
|
<div
|
||||||
|
className='h-3 rounded-full bg-slate-950'
|
||||||
|
style={{ width: `${activeReport.score}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='grid gap-6 p-6 md:grid-cols-2 md:p-8'>
|
||||||
|
<div className='rounded-3xl bg-slate-50 p-5'>
|
||||||
|
<h3 className='font-black'>Ringkasan sinyal</h3>
|
||||||
|
<dl className='mt-4 space-y-3 text-sm'>
|
||||||
|
<div className='flex justify-between gap-4'>
|
||||||
|
<dt className='text-slate-500'>Dokumen</dt>
|
||||||
|
<dd className='font-bold'>
|
||||||
|
{activeReport.verifiedDocuments}/4
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between gap-4'>
|
||||||
|
<dt className='text-slate-500'>Deep Dive Links</dt>
|
||||||
|
<dd className='font-bold'>
|
||||||
|
{activeReport.citedSources}/5
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between gap-4'>
|
||||||
|
<dt className='text-slate-500'>Akses Pulse</dt>
|
||||||
|
<dd className='font-bold'>
|
||||||
|
{activeReport.score >= 31
|
||||||
|
? 'Memenuhi syarat'
|
||||||
|
: 'Terkunci'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-between gap-4'>
|
||||||
|
<dt className='text-slate-500'>Integritas ulasan</dt>
|
||||||
|
<dd className='font-bold'>
|
||||||
|
{activeReport.verifiedReviews
|
||||||
|
? 'Terlindungi'
|
||||||
|
: 'Terbuka'}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div className='rounded-3xl bg-slate-950 p-5 text-white'>
|
||||||
|
<h3 className='font-black'>Aksi terbaik berikutnya</h3>
|
||||||
|
<ul className='mt-4 space-y-3 text-sm text-slate-300'>
|
||||||
|
{activeReport.recommendations.map((recommendation) => (
|
||||||
|
<li key={recommendation} className='flex gap-3'>
|
||||||
|
<span className='mt-1 h-2 w-2 flex-none rounded-full bg-[#1DE9B6]' />
|
||||||
|
<span>{recommendation}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='p-10 text-center'>
|
||||||
|
<div className='mx-auto mb-5 grid h-16 w-16 place-items-center rounded-3xl bg-slate-100 text-2xl'>
|
||||||
|
🛡️
|
||||||
|
</div>
|
||||||
|
<h2 className='text-2xl font-black'>
|
||||||
|
Detail laporan akan muncul di sini
|
||||||
|
</h2>
|
||||||
|
<p className='mt-3 text-slate-500'>
|
||||||
|
Buat Laporan Kepercayaan pertama untuk melihat tier,
|
||||||
|
progress bar, dan rekomendasi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer className='border-t border-white/10 px-5 py-8 text-sm text-slate-400 sm:px-8 lg:px-12'>
|
||||||
|
<div className='mx-auto flex max-w-7xl flex-col gap-4 md:flex-row md:items-center md:justify-between'>
|
||||||
|
<p>
|
||||||
|
© 2026 VORTA Universe MVP. Dimulai dari 01 Mega Super-App untuk
|
||||||
|
chat, sosial, marketplace, dan dompet ringan.
|
||||||
|
</p>
|
||||||
|
<div className='flex gap-5'>
|
||||||
|
<Link href='/privacy-policy/' className='hover:text-white'>
|
||||||
|
Kebijakan Privasi
|
||||||
|
</Link>
|
||||||
|
<Link href='/terms-of-use/' className='hover:text-white'>
|
||||||
|
Ketentuan Penggunaan
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href='/login'
|
||||||
|
className='font-semibold text-cyan-200 hover:text-white'
|
||||||
|
>
|
||||||
|
Panel Admin
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
VortaLanding.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>;
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export default function Login() {
|
|||||||
password: '9654d2ec',
|
password: '9654d2ec',
|
||||||
remember: true })
|
remember: true })
|
||||||
|
|
||||||
const title = 'Vorta Universe MVP'
|
const title = 'VORTA-COMMERCE'
|
||||||
|
|
||||||
// Fetch Pexels image/video
|
// Fetch Pexels image/video
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
@ -65,7 +65,7 @@ export default function Login() {
|
|||||||
// Redirect to dashboard if user is logged in
|
// Redirect to dashboard if user is logged in
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentUser?.id) {
|
if (currentUser?.id) {
|
||||||
router.push('/dashboard');
|
router.push('/vorta-commerce');
|
||||||
}
|
}
|
||||||
}, [currentUser?.id, router]);
|
}, [currentUser?.id, router]);
|
||||||
// Show error message if there is one
|
// Show error message if there is one
|
||||||
|
|||||||
660
frontend/src/pages/mega-super-app.tsx
Normal file
660
frontend/src/pages/mega-super-app.tsx
Normal file
@ -0,0 +1,660 @@
|
|||||||
|
import * as icon from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
|
type BusinessMessage = {
|
||||||
|
id: number;
|
||||||
|
sender: string;
|
||||||
|
text: string;
|
||||||
|
time: string;
|
||||||
|
mine?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SocialPost = {
|
||||||
|
id: number;
|
||||||
|
author: string;
|
||||||
|
text: string;
|
||||||
|
tag: string;
|
||||||
|
likes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Product = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
seller: string;
|
||||||
|
price: number;
|
||||||
|
badge: string;
|
||||||
|
stock: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CartItem = Product & {
|
||||||
|
qty: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type WalletTransaction = {
|
||||||
|
id: number;
|
||||||
|
label: string;
|
||||||
|
amount: number;
|
||||||
|
type: 'in' | 'out';
|
||||||
|
note: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Activity = {
|
||||||
|
id: number;
|
||||||
|
module: string;
|
||||||
|
text: string;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackIcon = icon.mdiViewDashboardOutline;
|
||||||
|
const resolveIcon = (name: string) =>
|
||||||
|
(name in icon ? icon[name as keyof typeof icon] : fallbackIcon) as string;
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) =>
|
||||||
|
new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
|
||||||
|
const MegaSuperAppPage = () => {
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||||
|
|
||||||
|
const [activeConversation, setActiveConversation] = React.useState('Mitra Grosir Bandung');
|
||||||
|
const [messageInput, setMessageInput] = React.useState('');
|
||||||
|
const [postInput, setPostInput] = React.useState('');
|
||||||
|
const [walletInput, setWalletInput] = React.useState('25000');
|
||||||
|
const [walletNote, setWalletNote] = React.useState('Top up saldo operasional');
|
||||||
|
const [balance, setBalance] = React.useState(1250000);
|
||||||
|
|
||||||
|
const [messages, setMessages] = React.useState<BusinessMessage[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
sender: 'Mitra Grosir Bandung',
|
||||||
|
text: 'Stok paket reseller sudah siap. Mau langsung dibuatkan invoice marketplace?',
|
||||||
|
time: '09:12',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
sender: 'Anda',
|
||||||
|
text: 'Ya, kirim 12 paket. Pembayaran saya pakai dompet workspace.',
|
||||||
|
time: '09:15',
|
||||||
|
mine: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
sender: 'Mitra Grosir Bandung',
|
||||||
|
text: 'Siap. Saya tautkan order ke chat ini supaya tracking reputasi ikut naik.',
|
||||||
|
time: '09:17',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [posts, setPosts] = React.useState<SocialPost[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
author: 'Komunitas Seller Lokal',
|
||||||
|
text: 'Promo bareng akhir pekan: bundling produk komunitas, cashback dompet, dan diskusi live di feed.',
|
||||||
|
tag: 'Kolaborasi',
|
||||||
|
likes: 42,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
author: 'Rina Kopi Nusantara',
|
||||||
|
text: 'Baru upload katalog kopi 250gr. Pembeli dari chat bisnis bisa checkout tanpa pindah aplikasi.',
|
||||||
|
tag: 'Marketplace',
|
||||||
|
likes: 27,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const products: Product[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Paket Reseller Kopi 12 pcs',
|
||||||
|
seller: 'Kopi Nusantara',
|
||||||
|
price: 360000,
|
||||||
|
badge: 'Terhubung chat',
|
||||||
|
stock: 18,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Voucher Iklan Feed Komunitas',
|
||||||
|
seller: '01 Ads Lite',
|
||||||
|
price: 150000,
|
||||||
|
badge: 'Naikkan reputasi',
|
||||||
|
stock: 40,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Jasa Desain Katalog UMKM',
|
||||||
|
seller: 'Studio Pasar Sosial',
|
||||||
|
price: 275000,
|
||||||
|
badge: 'Escrow dompet',
|
||||||
|
stock: 7,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const [cart, setCart] = React.useState<CartItem[]>([
|
||||||
|
{ ...products[0], qty: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [transactions, setTransactions] = React.useState<WalletTransaction[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
label: 'Pembayaran order kopi',
|
||||||
|
amount: 360000,
|
||||||
|
type: 'out',
|
||||||
|
note: 'Marketplace • escrow aktif',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
label: 'Cashback komunitas seller',
|
||||||
|
amount: 50000,
|
||||||
|
type: 'in',
|
||||||
|
note: 'Reward sosial',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
label: 'Top up dompet',
|
||||||
|
amount: 500000,
|
||||||
|
type: 'in',
|
||||||
|
note: 'Bank transfer',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [activities, setActivities] = React.useState<Activity[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
module: 'Chat',
|
||||||
|
text: 'Negosiasi dengan Mitra Grosir Bandung ditautkan ke order #MSA-1201.',
|
||||||
|
time: 'Baru saja',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
module: 'Marketplace',
|
||||||
|
text: '1 produk masuk keranjang dan siap dibayar dari Dompet 01.',
|
||||||
|
time: '3 menit lalu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
module: 'Sosial',
|
||||||
|
text: 'Posting komunitas seller mendapat 42 suka dan 8 calon pembeli.',
|
||||||
|
time: '12 menit lalu',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userName = currentUser?.firstName || currentUser?.email || 'Pengguna 01';
|
||||||
|
const cartTotal = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
|
||||||
|
const projectedBalance = balance - cartTotal;
|
||||||
|
|
||||||
|
const addActivity = (module: string, text: string) => {
|
||||||
|
setActivities((current) => [
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
module,
|
||||||
|
text,
|
||||||
|
time: 'Baru saja',
|
||||||
|
},
|
||||||
|
...current.slice(0, 5),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
const trimmed = messageInput.trim();
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((current) => [
|
||||||
|
...current,
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
sender: 'Anda',
|
||||||
|
text: trimmed,
|
||||||
|
time: 'Sekarang',
|
||||||
|
mine: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setMessageInput('');
|
||||||
|
addActivity('Chat', `Pesan bisnis baru dikirim ke ${activeConversation}.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const publishPost = () => {
|
||||||
|
const trimmed = postInput.trim();
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosts((current) => [
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
author: String(userName),
|
||||||
|
text: trimmed,
|
||||||
|
tag: 'Update Bisnis',
|
||||||
|
likes: 0,
|
||||||
|
},
|
||||||
|
...current,
|
||||||
|
]);
|
||||||
|
setPostInput('');
|
||||||
|
addActivity('Sosial', 'Update baru diterbitkan ke feed komunitas.');
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToCart = (product: Product) => {
|
||||||
|
setCart((current) => {
|
||||||
|
const existing = current.find((item) => item.id === product.id);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return current.map((item) =>
|
||||||
|
item.id === product.id ? { ...item, qty: item.qty + 1 } : item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...current, { ...product, qty: 1 }];
|
||||||
|
});
|
||||||
|
addActivity('Marketplace', `${product.name} ditambahkan ke keranjang terpadu.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkoutCart = () => {
|
||||||
|
if (!cart.length || cartTotal > balance) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBalance((current) => current - cartTotal);
|
||||||
|
setTransactions((current) => [
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
label: 'Checkout marketplace terpadu',
|
||||||
|
amount: cartTotal,
|
||||||
|
type: 'out',
|
||||||
|
note: `${cart.length} item • escrow dompet`,
|
||||||
|
},
|
||||||
|
...current,
|
||||||
|
]);
|
||||||
|
setCart([]);
|
||||||
|
addActivity('Dompet', `Checkout ${formatCurrency(cartTotal)} berhasil memakai Dompet 01.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addWalletTransaction = () => {
|
||||||
|
const amount = Number(walletInput);
|
||||||
|
const trimmedNote = walletNote.trim() || 'Top up dompet workspace';
|
||||||
|
|
||||||
|
if (!amount || amount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBalance((current) => current + amount);
|
||||||
|
setTransactions((current) => [
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
label: 'Top up cepat',
|
||||||
|
amount,
|
||||||
|
type: 'in',
|
||||||
|
note: trimmedNote,
|
||||||
|
},
|
||||||
|
...current,
|
||||||
|
]);
|
||||||
|
setWalletInput('');
|
||||||
|
setWalletNote('');
|
||||||
|
addActivity('Dompet', `Saldo bertambah ${formatCurrency(amount)} dari top up cepat.`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const conversations = [
|
||||||
|
{
|
||||||
|
name: 'Mitra Grosir Bandung',
|
||||||
|
status: 'Invoice marketplace siap',
|
||||||
|
unread: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Komunitas Seller Lokal',
|
||||||
|
status: 'Diskusi promo bareng',
|
||||||
|
unread: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Customer Prioritas',
|
||||||
|
status: 'Butuh update pengiriman',
|
||||||
|
unread: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Mega Super-App')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton
|
||||||
|
icon={resolveIcon('mdiApps')}
|
||||||
|
title="Mega Super-App"
|
||||||
|
main
|
||||||
|
>
|
||||||
|
<BaseButton
|
||||||
|
href="/vorta-commerce"
|
||||||
|
icon={icon.mdiViewDashboardOutline}
|
||||||
|
label="Kembali ke Dasbor"
|
||||||
|
color="whiteDark"
|
||||||
|
/>
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<CardBox className="mb-6 overflow-hidden" hasComponentLayout>
|
||||||
|
<div className="grid grid-cols-1 gap-0 lg:grid-cols-5">
|
||||||
|
<div className="p-6 lg:col-span-3">
|
||||||
|
<div className="mb-3 inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-sm font-semibold text-blue-700 dark:bg-blue-900/30 dark:text-blue-200">
|
||||||
|
<BaseIcon path={resolveIcon('mdiFlashOutline')} className="mr-2" size={18} />
|
||||||
|
Workspace ringan untuk bisnis, sosial, marketplace, dan dompet
|
||||||
|
</div>
|
||||||
|
<h2 className="mb-3 text-3xl font-bold leading-tight md:text-4xl">
|
||||||
|
Halo {userName}, kelola semua aktivitas dari satu ruang kerja.
|
||||||
|
</h2>
|
||||||
|
<p className="mb-5 max-w-3xl text-gray-600 dark:text-gray-300">
|
||||||
|
MVP ini menyatukan chat bisnis, feed komunitas, katalog marketplace,
|
||||||
|
keranjang, pembayaran dompet, dan aktivitas terpadu tanpa pindah halaman.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||||
|
{[
|
||||||
|
{ label: 'Chat aktif', value: conversations.length, iconName: 'mdiChatProcessingOutline' },
|
||||||
|
{ label: 'Posting sosial', value: posts.length, iconName: 'mdiPostOutline' },
|
||||||
|
{ label: 'Produk live', value: products.length, iconName: 'mdiStorefrontOutline' },
|
||||||
|
{ label: 'Saldo dompet', value: formatCurrency(balance), iconName: 'mdiWalletOutline' },
|
||||||
|
].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.label}
|
||||||
|
className="rounded-2xl border border-gray-100 bg-white/70 p-4 shadow-sm dark:border-dark-700 dark:bg-dark-800/60"
|
||||||
|
>
|
||||||
|
<BaseIcon path={resolveIcon(item.iconName)} className={`${iconsColor} mb-3`} size={26} />
|
||||||
|
<div className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xl font-bold">{item.value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-gray-100 bg-gray-50 p-6 dark:border-dark-700 dark:bg-dark-800/70 lg:col-span-2 lg:border-l lg:border-t-0">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Status workspace</p>
|
||||||
|
<h3 className="text-xl font-semibold">Siap operasional</h3>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full bg-green-100 px-3 py-1 text-sm font-semibold text-green-700 dark:bg-green-900/30 dark:text-green-200">
|
||||||
|
Live MVP
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{activities.slice(0, 4).map((activity) => (
|
||||||
|
<div key={activity.id} className="rounded-xl bg-white p-3 dark:bg-dark-900">
|
||||||
|
<div className="mb-1 flex items-center justify-between text-sm">
|
||||||
|
<span className="font-semibold text-blue-700 dark:text-blue-300">{activity.module}</span>
|
||||||
|
<span className="text-gray-400">{activity.time}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">{activity.text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||||
|
<CardBox className="xl:col-span-2" hasComponentLayout>
|
||||||
|
<div className="grid min-h-[620px] grid-cols-1 lg:grid-cols-5">
|
||||||
|
<aside className="border-b border-gray-100 p-4 dark:border-dark-700 lg:col-span-2 lg:border-b-0 lg:border-r">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">Chat Bisnis</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Percakapan, order, dan dompet tersambung.</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiChat')} className={iconsColor} size={30} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{conversations.map((conversation) => (
|
||||||
|
<button
|
||||||
|
key={conversation.name}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveConversation(conversation.name)}
|
||||||
|
className={`w-full rounded-2xl border p-3 text-left transition ${
|
||||||
|
activeConversation === conversation.name
|
||||||
|
? 'border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20'
|
||||||
|
: 'border-gray-100 bg-white hover:border-blue-200 dark:border-dark-700 dark:bg-dark-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-semibold">{conversation.name}</span>
|
||||||
|
<span className="rounded-full bg-orange-100 px-2 py-0.5 text-xs font-bold text-orange-700 dark:bg-orange-900/30 dark:text-orange-200">
|
||||||
|
{conversation.unread}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{conversation.status}</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 rounded-2xl bg-gray-50 p-4 dark:bg-dark-800">
|
||||||
|
<p className="mb-2 text-sm font-semibold text-gray-600 dark:text-gray-300">Aksi cepat</p>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<BaseButton label="Buat Order" icon={resolveIcon('mdiReceiptTextPlusOutline')} color="info" small />
|
||||||
|
<BaseButton label="Kirim Invoice" icon={resolveIcon('mdiFileSendOutline')} color="success" small />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="flex flex-col lg:col-span-3">
|
||||||
|
<div className="border-b border-gray-100 p-4 dark:border-dark-700">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Ruang chat aktif</p>
|
||||||
|
<h3 className="text-xl font-semibold">{activeConversation}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 space-y-3 bg-gray-50 p-4 dark:bg-dark-800/50">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<div key={message.id} className={`flex ${message.mine ? 'justify-end' : 'justify-start'}`}>
|
||||||
|
<div
|
||||||
|
className={`max-w-[82%] rounded-2xl px-4 py-3 shadow-sm ${
|
||||||
|
message.mine
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-white text-gray-800 dark:bg-dark-900 dark:text-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className={`mb-1 text-xs ${message.mine ? 'text-blue-100' : 'text-gray-400'}`}>
|
||||||
|
{message.sender} • {message.time}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm leading-relaxed">{message.text}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-gray-100 p-4 dark:border-dark-700">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
value={messageInput}
|
||||||
|
onChange={(event) => setMessageInput(event.target.value)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
sendMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="h-11 flex-1 rounded-xl border border-gray-200 bg-white px-3 text-sm outline-none focus:border-blue-400 focus:ring dark:border-dark-700 dark:bg-dark-900"
|
||||||
|
placeholder="Tulis pesan, buat invoice, atau tautkan produk..."
|
||||||
|
/>
|
||||||
|
<BaseButton label="Kirim" icon={resolveIcon('mdiSend')} color="info" onClick={sendMessage} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<CardBox hasComponentLayout>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">Dompet 01</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Saldo, top up, checkout, dan escrow.</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiWalletOutline')} className={iconsColor} size={32} />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl bg-gradient-to-br from-blue-600 to-emerald-500 p-5 text-white shadow-lg">
|
||||||
|
<p className="text-sm opacity-80">Saldo tersedia</p>
|
||||||
|
<div className="mt-1 text-3xl font-bold">{formatCurrency(balance)}</div>
|
||||||
|
<p className="mt-3 text-sm opacity-90">Estimasi setelah checkout: {formatCurrency(projectedBalance)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||||
|
<input
|
||||||
|
value={walletInput}
|
||||||
|
onChange={(event) => setWalletInput(event.target.value)}
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
className="h-10 rounded-xl border border-gray-200 bg-white px-3 text-sm outline-none focus:border-blue-400 focus:ring dark:border-dark-700 dark:bg-dark-900"
|
||||||
|
placeholder="Nominal top up"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={walletNote}
|
||||||
|
onChange={(event) => setWalletNote(event.target.value)}
|
||||||
|
className="h-10 rounded-xl border border-gray-200 bg-white px-3 text-sm outline-none focus:border-blue-400 focus:ring dark:border-dark-700 dark:bg-dark-900"
|
||||||
|
placeholder="Catatan"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
label="Top Up Cepat"
|
||||||
|
icon={resolveIcon('mdiPlusCircleOutline')}
|
||||||
|
color="success"
|
||||||
|
className="mt-3 w-full"
|
||||||
|
onClick={addWalletTransaction}
|
||||||
|
/>
|
||||||
|
<div className="mt-5 space-y-3">
|
||||||
|
{transactions.slice(0, 4).map((transaction) => (
|
||||||
|
<div key={transaction.id} className="flex items-start justify-between rounded-2xl bg-gray-50 p-3 dark:bg-dark-800">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">{transaction.label}</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">{transaction.note}</p>
|
||||||
|
</div>
|
||||||
|
<span className={`font-bold ${transaction.type === 'in' ? 'text-green-600' : 'text-red-500'}`}>
|
||||||
|
{transaction.type === 'in' ? '+' : '-'}{formatCurrency(transaction.amount)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||||
|
<CardBox hasComponentLayout>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">Feed Sosial Bisnis</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Bangun komunitas dan reputasi sambil menjual.</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiAccountVoice')} className={iconsColor} size={32} />
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
value={postInput}
|
||||||
|
onChange={(event) => setPostInput(event.target.value)}
|
||||||
|
className="h-24 w-full rounded-2xl border border-gray-200 bg-white p-3 text-sm outline-none focus:border-blue-400 focus:ring dark:border-dark-700 dark:bg-dark-900"
|
||||||
|
placeholder="Tulis update komunitas, promo sosial, atau cerita pelanggan..."
|
||||||
|
/>
|
||||||
|
<div className="mt-3 flex justify-end">
|
||||||
|
<BaseButton label="Posting ke Feed" icon={resolveIcon('mdiPostOutline')} color="info" onClick={publishPost} />
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 space-y-4">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<div key={post.id} className="rounded-2xl border border-gray-100 p-4 dark:border-dark-700">
|
||||||
|
<div className="mb-2 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">{post.author}</p>
|
||||||
|
<span className="text-xs font-semibold uppercase tracking-wide text-blue-600 dark:text-blue-300">{post.tag}</span>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex items-center rounded-full bg-gray-50 px-3 py-1 text-sm dark:bg-dark-800">
|
||||||
|
<BaseIcon path={resolveIcon('mdiHeartOutline')} className="mr-1 text-red-500" size={16} />
|
||||||
|
{post.likes}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300">{post.text}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox hasComponentLayout>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">Marketplace Ringan</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Produk, chat seller, keranjang, dan pembayaran dompet.</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiStorefrontOutline')} className={iconsColor} size={32} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:grid-cols-1 2xl:grid-cols-3">
|
||||||
|
{products.map((product) => (
|
||||||
|
<div key={product.id} className="rounded-2xl border border-gray-100 p-4 dark:border-dark-700">
|
||||||
|
<span className="mb-3 inline-flex rounded-full bg-orange-50 px-3 py-1 text-xs font-bold text-orange-700 dark:bg-orange-900/30 dark:text-orange-200">
|
||||||
|
{product.badge}
|
||||||
|
</span>
|
||||||
|
<h4 className="min-h-[48px] font-semibold">{product.name}</h4>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">{product.seller} • Stok {product.stock}</p>
|
||||||
|
<div className="mt-3 text-xl font-bold">{formatCurrency(product.price)}</div>
|
||||||
|
<BaseButton
|
||||||
|
label="Tambah"
|
||||||
|
icon={resolveIcon('mdiCartPlus')}
|
||||||
|
color="success"
|
||||||
|
small
|
||||||
|
className="mt-3 w-full"
|
||||||
|
onClick={() => addToCart(product)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 rounded-3xl bg-gray-50 p-4 dark:bg-dark-800">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<h4 className="font-semibold">Keranjang Terpadu</h4>
|
||||||
|
<span className="text-sm text-gray-500 dark:text-gray-400">{cart.length} item</span>
|
||||||
|
</div>
|
||||||
|
{cart.length ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{cart.map((item) => (
|
||||||
|
<div key={item.id} className="flex items-center justify-between text-sm">
|
||||||
|
<span>{item.name} × {item.qty}</span>
|
||||||
|
<span className="font-semibold">{formatCurrency(item.price * item.qty)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Keranjang kosong. Tambahkan produk untuk checkout.</p>
|
||||||
|
)}
|
||||||
|
<div className="mt-4 flex items-center justify-between border-t border-gray-200 pt-3 dark:border-dark-700">
|
||||||
|
<span className="font-semibold">Total</span>
|
||||||
|
<span className="text-xl font-bold">{formatCurrency(cartTotal)}</span>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
label={cartTotal > balance ? 'Saldo Tidak Cukup' : 'Bayar dari Dompet'}
|
||||||
|
icon={resolveIcon('mdiWalletPlusOutline')}
|
||||||
|
color={cartTotal > balance ? 'danger' : 'info'}
|
||||||
|
disabled={!cart.length || cartTotal > balance}
|
||||||
|
className="mt-3 w-full"
|
||||||
|
onClick={checkoutCart}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
MegaSuperAppPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MegaSuperAppPage;
|
||||||
@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
const title = 'Vorta Universe MVP'
|
const title = 'VORTA-COMMERCE'
|
||||||
const [projectUrl, setProjectUrl] = useState('');
|
const [projectUrl, setProjectUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import React, { ReactElement, useEffect, useState } from 'react';
|
import React, { ReactElement, useEffect, useState } from 'react';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import { useAppDispatch } from '../stores/hooks';
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
import { useAppSelector } from '../stores/hooks';
|
|
||||||
|
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
@ -76,7 +74,7 @@ const SearchView = () => {
|
|||||||
<BaseButton
|
<BaseButton
|
||||||
color='info'
|
color='info'
|
||||||
label='Back'
|
label='Back'
|
||||||
onClick={() => router.push('/dashboard')}
|
onClick={() => router.push('/vorta-commerce')}
|
||||||
/>
|
/>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
|
|||||||
import { getPageTitle } from '../config';
|
import { getPageTitle } from '../config';
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
const title = 'Vorta Universe MVP';
|
const title = 'VORTA-COMMERCE';
|
||||||
const [projectUrl, setProjectUrl] = useState('');
|
const [projectUrl, setProjectUrl] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
1362
frontend/src/pages/vorta-commerce.tsx
Normal file
1362
frontend/src/pages/vorta-commerce.tsx
Normal file
File diff suppressed because it is too large
Load Diff
800
frontend/src/pages/vorta-synapse.tsx
Normal file
800
frontend/src/pages/vorta-synapse.tsx
Normal file
@ -0,0 +1,800 @@
|
|||||||
|
import * as icon from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { ReactElement } from 'react';
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
import BaseIcon from '../components/BaseIcon';
|
||||||
|
import CardBox from '../components/CardBox';
|
||||||
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||||
|
import SectionMain from '../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||||
|
import { getPageTitle } from '../config';
|
||||||
|
import { useAppSelector } from '../stores/hooks';
|
||||||
|
|
||||||
|
type SynapseNode = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
city: string;
|
||||||
|
type: 'Creator Hub' | 'Fulfillment' | 'Retail Partner' | 'Drop Point';
|
||||||
|
health: number;
|
||||||
|
capacity: number;
|
||||||
|
latency: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type AffiliatePartner = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
tier: string;
|
||||||
|
sales: number;
|
||||||
|
commissionRate: number;
|
||||||
|
status: 'Aktif' | 'Butuh Follow Up' | 'Review Fraud';
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreativeDrop = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
creator: string;
|
||||||
|
channel: string;
|
||||||
|
inventory: number;
|
||||||
|
sold: number;
|
||||||
|
margin: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LogisticsJob = {
|
||||||
|
id: number;
|
||||||
|
code: string;
|
||||||
|
destination: string;
|
||||||
|
status: 'Routing' | 'Dikirim' | 'Terkirim' | 'Tahan QC';
|
||||||
|
eta: string;
|
||||||
|
courier: string;
|
||||||
|
risk: 'Rendah' | 'Sedang' | 'Tinggi';
|
||||||
|
};
|
||||||
|
|
||||||
|
type LedgerEvent = {
|
||||||
|
id: number;
|
||||||
|
module: string;
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
time: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fallbackIcon = icon.mdiViewDashboardOutline;
|
||||||
|
const resolveIcon = (name: string) =>
|
||||||
|
(name in icon ? icon[name as keyof typeof icon] : fallbackIcon) as string;
|
||||||
|
|
||||||
|
const formatCurrency = (value: number) =>
|
||||||
|
new Intl.NumberFormat('id-ID', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'IDR',
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
|
||||||
|
const formatNumber = (value: number) =>
|
||||||
|
new Intl.NumberFormat('id-ID', {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
}).format(value);
|
||||||
|
|
||||||
|
const VortaSynapsePage = () => {
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
||||||
|
|
||||||
|
const [activeNodeId, setActiveNodeId] = React.useState(1);
|
||||||
|
const [campaignInput, setCampaignInput] = React.useState('Drop merchandise kreator lokal Batch 04');
|
||||||
|
const [selectedDropId, setSelectedDropId] = React.useState(1);
|
||||||
|
const [distributionBudget, setDistributionBudget] = React.useState('7500000');
|
||||||
|
const [commissionPool, setCommissionPool] = React.useState(18250000);
|
||||||
|
const [routingEfficiency, setRoutingEfficiency] = React.useState(86);
|
||||||
|
|
||||||
|
const [nodes, setNodes] = React.useState<SynapseNode[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Jakarta Creator Hub',
|
||||||
|
city: 'Jakarta',
|
||||||
|
type: 'Creator Hub',
|
||||||
|
health: 96,
|
||||||
|
capacity: 78,
|
||||||
|
latency: '12 ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Bandung Micro Fulfillment',
|
||||||
|
city: 'Bandung',
|
||||||
|
type: 'Fulfillment',
|
||||||
|
health: 91,
|
||||||
|
capacity: 64,
|
||||||
|
latency: '18 ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Surabaya Affiliate Gate',
|
||||||
|
city: 'Surabaya',
|
||||||
|
type: 'Retail Partner',
|
||||||
|
health: 88,
|
||||||
|
capacity: 72,
|
||||||
|
latency: '24 ms',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Bali Pop-Up Drop Point',
|
||||||
|
city: 'Denpasar',
|
||||||
|
type: 'Drop Point',
|
||||||
|
health: 83,
|
||||||
|
capacity: 55,
|
||||||
|
latency: '31 ms',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [partners, setPartners] = React.useState<AffiliatePartner[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Nara Studio Circle',
|
||||||
|
tier: 'Creator Lead',
|
||||||
|
sales: 146,
|
||||||
|
commissionRate: 14,
|
||||||
|
status: 'Aktif',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Kolektif Musik Indie',
|
||||||
|
tier: 'Community Partner',
|
||||||
|
sales: 92,
|
||||||
|
commissionRate: 11,
|
||||||
|
status: 'Aktif',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name: 'Micro Seller Timur',
|
||||||
|
tier: 'Regional Affiliate',
|
||||||
|
sales: 51,
|
||||||
|
commissionRate: 9,
|
||||||
|
status: 'Butuh Follow Up',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: 'Promo Flash Network',
|
||||||
|
tier: 'Performance',
|
||||||
|
sales: 18,
|
||||||
|
commissionRate: 7,
|
||||||
|
status: 'Review Fraud',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [drops, setDrops] = React.useState<CreativeDrop[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Hoodie Tur Konser Virtual',
|
||||||
|
creator: 'Aksara Nada',
|
||||||
|
channel: 'TikTok Shop + Komunitas',
|
||||||
|
inventory: 420,
|
||||||
|
sold: 287,
|
||||||
|
margin: 38,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Print Art Edisi 04',
|
||||||
|
creator: 'Maya Visual Lab',
|
||||||
|
channel: 'Marketplace Kolektor',
|
||||||
|
inventory: 180,
|
||||||
|
sold: 119,
|
||||||
|
margin: 44,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'Paket Kelas Mini Creator',
|
||||||
|
creator: 'Ruang Cerita',
|
||||||
|
channel: 'Affiliate Academy',
|
||||||
|
inventory: 650,
|
||||||
|
sold: 312,
|
||||||
|
margin: 52,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [jobs, setJobs] = React.useState<LogisticsJob[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
code: 'VSP-0401',
|
||||||
|
destination: 'Jakarta Selatan',
|
||||||
|
status: 'Routing',
|
||||||
|
eta: '2 jam',
|
||||||
|
courier: 'Vorta Route AI',
|
||||||
|
risk: 'Rendah',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
code: 'VSP-0402',
|
||||||
|
destination: 'Bandung Kota',
|
||||||
|
status: 'Dikirim',
|
||||||
|
eta: '5 jam',
|
||||||
|
courier: 'Synapse Express',
|
||||||
|
risk: 'Sedang',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
code: 'VSP-0403',
|
||||||
|
destination: 'Surabaya Timur',
|
||||||
|
status: 'Tahan QC',
|
||||||
|
eta: '12 jam',
|
||||||
|
courier: 'Partner Fleet',
|
||||||
|
risk: 'Tinggi',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [ledger, setLedger] = React.useState<LedgerEvent[]>([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
module: 'Distribusi',
|
||||||
|
title: 'Batch 04 disinkronkan',
|
||||||
|
detail: '287 unit hoodie dialokasikan dari Jakarta Creator Hub ke 3 node regional.',
|
||||||
|
time: 'Baru saja',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
module: 'Afiliasi',
|
||||||
|
title: 'Komisi kreator dihitung',
|
||||||
|
detail: 'Pool komisi minggu ini siap dibagi ke 4 partner aktif.',
|
||||||
|
time: '8 menit lalu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
module: 'Logistik',
|
||||||
|
title: 'Rute risiko tinggi ditahan',
|
||||||
|
detail: 'Paket VSP-0403 masuk antrian QC sebelum dilanjutkan ke Surabaya Timur.',
|
||||||
|
time: '14 menit lalu',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userName = currentUser?.firstName || currentUser?.email || 'Operator Vorta';
|
||||||
|
const activeNode = nodes.find((node) => node.id === activeNodeId) || nodes[0];
|
||||||
|
const selectedDrop = drops.find((drop) => drop.id === selectedDropId) || drops[0];
|
||||||
|
const totalInventory = drops.reduce((sum, drop) => sum + drop.inventory, 0);
|
||||||
|
const totalSold = drops.reduce((sum, drop) => sum + drop.sold, 0);
|
||||||
|
const sellThrough = Math.round((totalSold / totalInventory) * 100);
|
||||||
|
const partnerSales = partners.reduce((sum, partner) => sum + partner.sales, 0);
|
||||||
|
const averageCommission = Math.round(
|
||||||
|
partners.reduce((sum, partner) => sum + partner.commissionRate, 0) / partners.length,
|
||||||
|
);
|
||||||
|
const openJobs = jobs.filter((job) => job.status !== 'Terkirim').length;
|
||||||
|
|
||||||
|
const addLedgerEvent = (module: string, title: string, detail: string) => {
|
||||||
|
setLedger((current) => [
|
||||||
|
{
|
||||||
|
id: Date.now(),
|
||||||
|
module,
|
||||||
|
title,
|
||||||
|
detail,
|
||||||
|
time: 'Baru saja',
|
||||||
|
},
|
||||||
|
...current.slice(0, 6),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const launchCampaign = () => {
|
||||||
|
const trimmedCampaign = campaignInput.trim();
|
||||||
|
const budget = Number(distributionBudget);
|
||||||
|
|
||||||
|
if (!trimmedCampaign || !budget || budget <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextDrop: CreativeDrop = {
|
||||||
|
id: Date.now(),
|
||||||
|
title: trimmedCampaign,
|
||||||
|
creator: String(userName),
|
||||||
|
channel: `${activeNode.city} Synapse Node`,
|
||||||
|
inventory: Math.max(80, Math.round(budget / 50000)),
|
||||||
|
sold: 0,
|
||||||
|
margin: selectedDrop.margin,
|
||||||
|
};
|
||||||
|
|
||||||
|
setDrops((current) => [nextDrop, ...current]);
|
||||||
|
setSelectedDropId(nextDrop.id);
|
||||||
|
setCommissionPool((current) => current + Math.round(budget * 0.18));
|
||||||
|
setCampaignInput('');
|
||||||
|
setDistributionBudget('');
|
||||||
|
addLedgerEvent(
|
||||||
|
'Distribusi',
|
||||||
|
'Kampanye kreator diluncurkan',
|
||||||
|
`${nextDrop.title} masuk node ${activeNode.name} dengan estimasi stok ${formatNumber(nextDrop.inventory)} unit.`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const optimizeRoute = () => {
|
||||||
|
setRoutingEfficiency((current) => Math.min(99, current + 3));
|
||||||
|
setNodes((current) =>
|
||||||
|
current.map((node) =>
|
||||||
|
node.id === activeNodeId
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
health: Math.min(99, node.health + 2),
|
||||||
|
capacity: Math.max(35, node.capacity - 4),
|
||||||
|
}
|
||||||
|
: node,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setJobs((current) =>
|
||||||
|
current.map((job, index) =>
|
||||||
|
index === 0
|
||||||
|
? {
|
||||||
|
...job,
|
||||||
|
status: 'Dikirim',
|
||||||
|
eta: '90 menit',
|
||||||
|
risk: 'Rendah',
|
||||||
|
}
|
||||||
|
: job,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
addLedgerEvent(
|
||||||
|
'Logistik',
|
||||||
|
'Rute pintar dioptimalkan',
|
||||||
|
`${activeNode.name} menurunkan ETA prioritas dan meningkatkan efisiensi rute menjadi ${Math.min(99, routingEfficiency + 3)}%.`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const settleCommissions = () => {
|
||||||
|
if (commissionPool <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPartners((current) =>
|
||||||
|
current.map((partner) => ({
|
||||||
|
...partner,
|
||||||
|
sales: partner.sales + Math.round(partner.sales * 0.06),
|
||||||
|
status: partner.status === 'Review Fraud' ? 'Butuh Follow Up' : 'Aktif',
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
addLedgerEvent(
|
||||||
|
'Afiliasi',
|
||||||
|
'Settlement komisi diproses',
|
||||||
|
`${formatCurrency(commissionPool)} dialokasikan ke partner dengan rata-rata komisi ${averageCommission}%.`,
|
||||||
|
);
|
||||||
|
setCommissionPool(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatchShipment = (jobId: number) => {
|
||||||
|
setJobs((current) =>
|
||||||
|
current.map((job) =>
|
||||||
|
job.id === jobId
|
||||||
|
? {
|
||||||
|
...job,
|
||||||
|
status: job.status === 'Terkirim' ? 'Terkirim' : 'Terkirim',
|
||||||
|
eta: 'Selesai',
|
||||||
|
risk: 'Rendah',
|
||||||
|
}
|
||||||
|
: job,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const completedJob = jobs.find((job) => job.id === jobId);
|
||||||
|
|
||||||
|
if (completedJob) {
|
||||||
|
addLedgerEvent(
|
||||||
|
'Logistik',
|
||||||
|
'Paket diselesaikan',
|
||||||
|
`${completedJob.code} ke ${completedJob.destination} ditandai terkirim dan siap settlement kreator.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const boostSelectedDrop = () => {
|
||||||
|
setDrops((current) =>
|
||||||
|
current.map((drop) =>
|
||||||
|
drop.id === selectedDropId
|
||||||
|
? {
|
||||||
|
...drop,
|
||||||
|
sold: Math.min(drop.inventory, drop.sold + 24),
|
||||||
|
margin: Math.min(70, drop.margin + 1),
|
||||||
|
}
|
||||||
|
: drop,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
addLedgerEvent(
|
||||||
|
'Distribusi',
|
||||||
|
'Afiliasi boost diterapkan',
|
||||||
|
`${selectedDrop.title} mendapat 24 proyeksi penjualan baru lewat partner performa tinggi.`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('04 Vorta Synapse')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton
|
||||||
|
icon={resolveIcon('mdiTransitConnectionVariant')}
|
||||||
|
title="04 Vorta Synapse"
|
||||||
|
main
|
||||||
|
>
|
||||||
|
<BaseButton
|
||||||
|
href="/vorta-commerce"
|
||||||
|
icon={icon.mdiViewDashboardOutline}
|
||||||
|
label="Kembali ke Dasbor"
|
||||||
|
color="whiteDark"
|
||||||
|
/>
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<CardBox className="mb-6 overflow-hidden" hasComponentLayout>
|
||||||
|
<div className="grid grid-cols-1 gap-0 xl:grid-cols-5">
|
||||||
|
<div className="relative overflow-hidden p-6 xl:col-span-3">
|
||||||
|
<div className="absolute right-0 top-0 h-48 w-48 rounded-full bg-blue-200/30 blur-3xl dark:bg-blue-900/30" />
|
||||||
|
<div className="absolute bottom-0 right-28 h-36 w-36 rounded-full bg-green-200/30 blur-3xl dark:bg-green-900/30" />
|
||||||
|
<div className="relative">
|
||||||
|
<div className="mb-3 inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-sm font-semibold text-blue-700 dark:bg-blue-900/30 dark:text-blue-200">
|
||||||
|
<BaseIcon path={resolveIcon('mdiNetworkOutline')} className="mr-2" size={18} />
|
||||||
|
Protokol distribusi, afiliasi, dan logistik pintar untuk ekonomi kreator
|
||||||
|
</div>
|
||||||
|
<h2 className="mb-3 text-3xl font-bold leading-tight md:text-4xl">
|
||||||
|
Halo {userName}, sinkronkan drop kreator dari kampanye sampai paket terkirim.
|
||||||
|
</h2>
|
||||||
|
<p className="mb-5 max-w-3xl text-gray-600 dark:text-gray-300">
|
||||||
|
Vorta Synapse menyatukan node distribusi, partner afiliasi, pool komisi,
|
||||||
|
alokasi stok, dan rute logistik adaptif agar kreator bisa menjual lebih luas
|
||||||
|
tanpa kehilangan kontrol operasional.
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||||
|
{[
|
||||||
|
{ label: 'Sell-through', value: `${sellThrough}%`, iconName: 'mdiChartTimelineVariant' },
|
||||||
|
{ label: 'Penjualan afiliasi', value: formatNumber(partnerSales), iconName: 'mdiAccountMultipleCheckOutline' },
|
||||||
|
{ label: 'Komisi siap', value: formatCurrency(commissionPool), iconName: 'mdiCashSync' },
|
||||||
|
{ label: 'Job logistik', value: openJobs, iconName: 'mdiTruckFastOutline' },
|
||||||
|
].map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.label}
|
||||||
|
className="rounded-2xl border border-gray-100 bg-white/70 p-4 shadow-sm dark:border-dark-700 dark:bg-dark-800/60"
|
||||||
|
>
|
||||||
|
<BaseIcon path={resolveIcon(item.iconName)} className={`${iconsColor} mb-3`} size={26} />
|
||||||
|
<div className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 text-xl font-bold">{item.value}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-gray-100 bg-gray-50 p-6 dark:border-dark-700 dark:bg-dark-800/70 xl:col-span-2 xl:border-l xl:border-t-0">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Synapse health</p>
|
||||||
|
<h3 className="text-xl font-semibold">{routingEfficiency}% route efficiency</h3>
|
||||||
|
</div>
|
||||||
|
<span className="rounded-full bg-green-100 px-3 py-1 text-sm font-semibold text-green-700 dark:bg-green-900/30 dark:text-green-200">
|
||||||
|
Protocol Live
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{ledger.slice(0, 4).map((event) => (
|
||||||
|
<div key={event.id} className="rounded-xl bg-white p-3 dark:bg-dark-900">
|
||||||
|
<div className="mb-1 flex items-center justify-between text-sm">
|
||||||
|
<span className="font-semibold text-blue-700 dark:text-blue-300">{event.module}</span>
|
||||||
|
<span className="text-gray-400">{event.time}</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-semibold">{event.title}</p>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-300">{event.detail}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
||||||
|
<CardBox className="xl:col-span-2" hasComponentLayout>
|
||||||
|
<div className="grid min-h-[650px] grid-cols-1 lg:grid-cols-5">
|
||||||
|
<aside className="border-b border-gray-100 p-4 dark:border-dark-700 lg:col-span-2 lg:border-b-0 lg:border-r">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">Synapse Nodes</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Pusat kreator, fulfillment, partner retail, dan drop point.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiAccessPointNetwork')} className={iconsColor} size={30} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{nodes.map((node) => (
|
||||||
|
<button
|
||||||
|
key={node.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveNodeId(node.id)}
|
||||||
|
className={`w-full rounded-2xl border p-3 text-left transition ${
|
||||||
|
activeNodeId === node.id
|
||||||
|
? 'border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20'
|
||||||
|
: 'border-gray-100 bg-white hover:border-blue-200 dark:border-dark-700 dark:bg-dark-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-semibold">{node.name}</span>
|
||||||
|
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-bold text-blue-700 dark:bg-blue-900/30 dark:text-blue-200">
|
||||||
|
{node.health}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{node.city} • {node.type} • Latensi {node.latency}
|
||||||
|
</p>
|
||||||
|
<div className="mt-3 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-dark-800">
|
||||||
|
<div className="h-full rounded-full bg-blue-500" style={{ width: `${node.capacity}%` }} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<section className="flex flex-col lg:col-span-3">
|
||||||
|
<div className="border-b border-gray-100 p-4 dark:border-dark-700">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Node aktif</p>
|
||||||
|
<div className="flex flex-col justify-between gap-3 md:flex-row md:items-center">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-semibold">{activeNode.name}</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{activeNode.type} di {activeNode.city} • kapasitas {activeNode.capacity}% terpakai
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
label="Optimalkan Rute"
|
||||||
|
icon={resolveIcon('mdiRoutes')}
|
||||||
|
color="info"
|
||||||
|
onClick={optimizeRoute}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 bg-gray-50 p-4 dark:bg-dark-800/50">
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<div className="rounded-3xl bg-white p-4 dark:bg-dark-900">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Health score</p>
|
||||||
|
<div className="mt-2 text-4xl font-bold text-green-600">{activeNode.health}%</div>
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Node siap menerima batch dan menjaga SLA fulfillment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl bg-white p-4 dark:bg-dark-900">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Routing efficiency</p>
|
||||||
|
<div className="mt-2 text-4xl font-bold text-blue-600">{routingEfficiency}%</div>
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Optimasi mempertimbangkan stok, risiko, jarak, dan kapasitas mitra.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl bg-white p-4 dark:bg-dark-900">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Average commission</p>
|
||||||
|
<div className="mt-2 text-4xl font-bold text-orange-500">{averageCommission}%</div>
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Tarif dinamis mengikuti performa dan kualitas traffic partner.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 rounded-3xl bg-white p-4 dark:bg-dark-900">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">Launcher kampanye kreator</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Buat batch distribusi baru dan isi pool afiliasi otomatis.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiRocketLaunchOutline')} className={iconsColor} size={28} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||||
|
<input
|
||||||
|
value={campaignInput}
|
||||||
|
onChange={(event) => setCampaignInput(event.target.value)}
|
||||||
|
className="h-11 rounded-xl border border-gray-200 bg-white px-3 text-sm outline-none focus:border-blue-400 focus:ring dark:border-dark-700 dark:bg-dark-800 md:col-span-2"
|
||||||
|
placeholder="Nama drop atau kampanye kreator"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={distributionBudget}
|
||||||
|
onChange={(event) => setDistributionBudget(event.target.value)}
|
||||||
|
className="h-11 rounded-xl border border-gray-200 bg-white px-3 text-sm outline-none focus:border-blue-400 focus:ring dark:border-dark-700 dark:bg-dark-800"
|
||||||
|
placeholder="Budget distribusi"
|
||||||
|
inputMode="numeric"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Template margin mengikuti {selectedDrop.title} ({selectedDrop.margin}%).
|
||||||
|
</p>
|
||||||
|
<BaseButton
|
||||||
|
label="Luncurkan Batch"
|
||||||
|
icon={resolveIcon('mdiPlusCircleOutline')}
|
||||||
|
color="success"
|
||||||
|
onClick={launchCampaign}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<CardBox hasComponentLayout>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">Komisi Afiliasi</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Pool komisi untuk kreator, komunitas, dan reseller.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiCashMultiple')} className={iconsColor} size={32} />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-3xl bg-gray-50 p-4 dark:bg-dark-800">
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">Pool siap settlement</p>
|
||||||
|
<div className="mt-1 text-3xl font-bold">{formatCurrency(commissionPool)}</div>
|
||||||
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Partner aktif: {partners.filter((partner) => partner.status === 'Aktif').length}/{partners.length}
|
||||||
|
</p>
|
||||||
|
<BaseButton
|
||||||
|
label="Settlement Komisi"
|
||||||
|
icon={resolveIcon('mdiBankTransferOut')}
|
||||||
|
color="info"
|
||||||
|
disabled={commissionPool <= 0}
|
||||||
|
className="mt-3 w-full"
|
||||||
|
onClick={settleCommissions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
{partners.map((partner) => (
|
||||||
|
<div key={partner.id} className="rounded-2xl border border-gray-100 p-3 dark:border-dark-700">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold">{partner.name}</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">{partner.tier}</p>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-2 py-1 text-xs font-bold ${
|
||||||
|
partner.status === 'Aktif'
|
||||||
|
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-200'
|
||||||
|
: partner.status === 'Review Fraud'
|
||||||
|
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-200'
|
||||||
|
: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{partner.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div className="rounded-xl bg-gray-50 p-2 dark:bg-dark-800">
|
||||||
|
<span className="text-gray-500 dark:text-gray-400">Sales</span>
|
||||||
|
<p className="font-semibold">{formatNumber(partner.sales)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-gray-50 p-2 dark:bg-dark-800">
|
||||||
|
<span className="text-gray-500 dark:text-gray-400">Rate</span>
|
||||||
|
<p className="font-semibold">{partner.commissionRate}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-2">
|
||||||
|
<CardBox hasComponentLayout>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">Drop Kreator</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Produk digital/fisik yang sedang didorong oleh protokol distribusi.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiPackageVariantClosed')} className={iconsColor} size={32} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:grid-cols-1 2xl:grid-cols-3">
|
||||||
|
{drops.map((drop) => {
|
||||||
|
const progress = Math.round((drop.sold / drop.inventory) * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={drop.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedDropId(drop.id)}
|
||||||
|
className={`rounded-2xl border p-4 text-left transition ${
|
||||||
|
selectedDropId === drop.id
|
||||||
|
? 'border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20'
|
||||||
|
: 'border-gray-100 hover:border-blue-200 dark:border-dark-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="mb-3 inline-flex rounded-full bg-orange-50 px-3 py-1 text-xs font-bold text-orange-700 dark:bg-orange-900/30 dark:text-orange-200">
|
||||||
|
Margin {drop.margin}%
|
||||||
|
</span>
|
||||||
|
<h4 className="min-h-[48px] font-semibold">{drop.title}</h4>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{drop.creator} • {drop.channel}
|
||||||
|
</p>
|
||||||
|
<div className="mt-3 flex items-center justify-between text-sm">
|
||||||
|
<span>{formatNumber(drop.sold)} / {formatNumber(drop.inventory)} terjual</span>
|
||||||
|
<span className="font-semibold">{progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-dark-800">
|
||||||
|
<div className="h-full rounded-full bg-green-500" style={{ width: `${progress}%` }} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-5 rounded-3xl bg-gray-50 p-4 dark:bg-dark-800">
|
||||||
|
<div className="mb-3 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold">Boost afiliasi terpilih</h4>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">{selectedDrop.title}</p>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
label="Boost 24 Sales"
|
||||||
|
icon={resolveIcon('mdiTrendingUp')}
|
||||||
|
color="success"
|
||||||
|
small
|
||||||
|
onClick={boostSelectedDrop}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Simulasi ini menambah proyeksi penjualan dan menaikkan margin drop pilihan.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<CardBox hasComponentLayout>
|
||||||
|
<div className="p-5">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold">Logistik Pintar</h3>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Queue pengiriman dengan ETA, risiko, dan aksi penyelesaian.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<BaseIcon path={resolveIcon('mdiTruckDeliveryOutline')} className={iconsColor} size={32} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{jobs.map((job) => (
|
||||||
|
<div key={job.id} className="rounded-2xl border border-gray-100 p-4 dark:border-dark-700">
|
||||||
|
<div className="flex flex-col justify-between gap-3 md:flex-row md:items-start">
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<h4 className="font-semibold">{job.code}</h4>
|
||||||
|
<span
|
||||||
|
className={`rounded-full px-2 py-1 text-xs font-bold ${
|
||||||
|
job.risk === 'Rendah'
|
||||||
|
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-200'
|
||||||
|
: job.risk === 'Tinggi'
|
||||||
|
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-200'
|
||||||
|
: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Risiko {job.risk}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{job.destination} • {job.courier}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-left md:text-right">
|
||||||
|
<p className="font-semibold">{job.status}</p>
|
||||||
|
<p className="text-sm text-gray-500 dark:text-gray-400">ETA {job.eta}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BaseButton
|
||||||
|
label={job.status === 'Terkirim' ? 'Sudah Terkirim' : 'Tandai Terkirim'}
|
||||||
|
icon={resolveIcon('mdiCheckCircleOutline')}
|
||||||
|
color={job.status === 'Terkirim' ? 'success' : 'info'}
|
||||||
|
small
|
||||||
|
disabled={job.status === 'Terkirim'}
|
||||||
|
className="mt-3 w-full"
|
||||||
|
onClick={() => dispatchShipment(job.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
VortaSynapsePage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VortaSynapsePage;
|
||||||
Loading…
x
Reference in New Issue
Block a user