Compare commits

...

No commits in common. "master" and "main" have entirely different histories.
master ... main

104 changed files with 6386 additions and 6343 deletions

39
.gitignore vendored
View File

@ -1,41 +1,4 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
/node_modules

View File

@ -1,40 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details.

17
eslint.config.js Normal file
View File

@ -0,0 +1,17 @@
import js from '@eslint/js'
import pluginVue from 'eslint-plugin-vue'
export default [
{
name: 'app/files-to-lint',
files: ['**/*.{js,mjs,jsx,vue}'],
},
{
name: 'app/files-to-ignore',
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
},
js.configs.recommended,
...pluginVue.configs['flat/essential'],
]

View File

@ -1,18 +0,0 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

19
index.html Normal file
View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"
rel="stylesheet"
/>
<title>Solnus | Legal Service Solutions</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View File

@ -1,9 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
reactCompiler: true,
reactStrictMode: true,
};
export default nextConfig;

4071
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,24 @@
{
"name": "wisatin",
"version": "0.1.0",
"name": "vue-project",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --fix"
},
"dependencies": {
"@ant-design/cssinjs": "^2.0.1",
"@hugeicons/core-free-icons": "^2.0.0",
"@hugeicons/react": "^1.1.1",
"antd": "^6.0.1",
"clsx": "^2.1.1",
"embla-carousel": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.556.0",
"next": "16.0.7",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-slick": "^0.31.0",
"slick-carousel": "^1.8.1",
"swiper": "^8.4.7",
"tailwind-merge": "^3.4.0"
"vue": "^3.5.13",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-slick": "^0.23.13",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"tailwindcss": "^4",
"typescript": "^5"
"@eslint/js": "^9.14.0",
"@vitejs/plugin-vue": "^5.2.1",
"eslint": "^9.14.0",
"eslint-plugin-vue": "^9.30.0",
"vite": "^6.0.5",
"vite-plugin-vue-devtools": "^7.6.8"
}
}

5060
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 MiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View File

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

22
src/App.vue Normal file
View File

@ -0,0 +1,22 @@
<script setup>
import BackToTop from "./components/button/BackToTop.vue";
import Footer from "./components/footer/Footer.vue";
import Navigation from "./components/Navigation.vue";
</script>
<template>
<header>
<Navigation />
</header>
<router-view />
<footer id="kontak">
<Footer />
</footer>
<BackToTop />
</template>
<style scoped>
</style>

View File

@ -1,13 +0,0 @@
import React, { PropsWithChildren, forwardRef } from 'react';
type BoxProps = React.HTMLAttributes<HTMLDivElement>;
const Box = forwardRef<HTMLDivElement, PropsWithChildren<BoxProps>>(({ children, ...props }, ref) => {
return (
<div ref={ref} {...props}>
{children}
</div>
);
});
export default Box;

View File

@ -1,23 +0,0 @@
import React from 'react';
import { SvgIconProps } from '@/Types/Icon';
import { cn } from '@/Functions/cn';
const AccountIcon: React.FC<SvgIconProps> = ({ width = '24', height = '30', fill = 'black', className }) => {
return (
<svg
viewBox="0 0 30 30"
fill="none"
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
className={cn([className])}
>
<path
d="M5.325 23.4375C6.9 22.3375 8.4625 21.4937 10.0125 20.9062C11.5625 20.3188 13.225 20.025 15 20.025C16.775 20.025 18.4438 20.3188 20.0062 20.9062C21.5687 21.4937 23.1375 22.3375 24.7125 23.4375C25.8125 22.0875 26.5938 20.725 27.0563 19.35C27.5188 17.975 27.75 16.525 27.75 15C27.75 11.375 26.5312 8.34375 24.0938 5.90625C21.6562 3.46875 18.625 2.25 15 2.25C11.375 2.25 8.34375 3.46875 5.90625 5.90625C3.46875 8.34375 2.25 11.375 2.25 15C2.25 16.525 2.4875 17.975 2.9625 19.35C3.4375 20.725 4.225 22.0875 5.325 23.4375ZM15 16.125C13.55 16.125 12.3313 15.6312 11.3438 14.6437C10.3562 13.6562 9.8625 12.4375 9.8625 10.9875C9.8625 9.5375 10.3562 8.31875 11.3438 7.33125C12.3313 6.34375 13.55 5.85 15 5.85C16.45 5.85 17.6688 6.34375 18.6562 7.33125C19.6437 8.31875 20.1375 9.5375 20.1375 10.9875C20.1375 12.4375 19.6437 13.6562 18.6562 14.6437C17.6688 15.6312 16.45 16.125 15 16.125ZM15 30C12.95 30 11.0125 29.6062 9.1875 28.8187C7.3625 28.0312 5.76875 26.9562 4.40625 25.5938C3.04375 24.2313 1.96875 22.6375 1.18125 20.8125C0.39375 18.9875 0 17.05 0 15C0 12.925 0.39375 10.9813 1.18125 9.16875C1.96875 7.35625 3.04375 5.76875 4.40625 4.40625C5.76875 3.04375 7.3625 1.96875 9.1875 1.18125C11.0125 0.39375 12.95 0 15 0C17.075 0 19.0187 0.39375 20.8312 1.18125C22.6437 1.96875 24.2313 3.04375 25.5938 4.40625C26.9562 5.76875 28.0312 7.35625 28.8187 9.16875C29.6062 10.9813 30 12.925 30 15C30 17.05 29.6062 18.9875 28.8187 20.8125C28.0312 22.6375 26.9562 24.2313 25.5938 25.5938C24.2313 26.9562 22.6437 28.0312 20.8312 28.8187C19.0187 29.6062 17.075 30 15 30ZM15 27.75C16.375 27.75 17.7188 27.55 19.0312 27.15C20.3438 26.75 21.6375 26.05 22.9125 25.05C21.6375 24.15 20.3375 23.4625 19.0125 22.9875C17.6875 22.5125 16.35 22.275 15 22.275C13.65 22.275 12.3125 22.5125 10.9875 22.9875C9.6625 23.4625 8.3625 24.15 7.0875 25.05C8.3625 26.05 9.65625 26.75 10.9688 27.15C12.2812 27.55 13.625 27.75 15 27.75ZM15 13.875C15.85 13.875 16.5437 13.6062 17.0812 13.0687C17.6187 12.5312 17.8875 11.8375 17.8875 10.9875C17.8875 10.1375 17.6187 9.44375 17.0812 8.90625C16.5437 8.36875 15.85 8.1 15 8.1C14.15 8.1 13.4563 8.36875 12.9188 8.90625C12.3813 9.44375 12.1125 10.1375 12.1125 10.9875C12.1125 11.8375 12.3813 12.5312 12.9188 13.0687C13.4563 13.6062 14.15 13.875 15 13.875Z"
fill={fill}
/>
</svg>
);
};
export default AccountIcon;

View File

@ -1,37 +0,0 @@
import Head from 'next/head';
export type SEOProps = {
title?: string;
description?: string;
img?: string;
imgAlt?: string;
};
export const SEO = ({ title, description, img, imgAlt }: SEOProps) => {
return (
<Head>
{title ? (
<>
<title>{title}</title>
<meta key="og:title" property="og:title" content={title} />
<meta key="twitter:title" property="twitter:title" content={title} />
</>
) : null}
{description ? (
<>
<meta key="description" name="description" content={description} />
<meta key="og:description" name="og:description" content={description} />
<meta key="twitter:description" name="twitter:description" content={description} />
</>
) : null}
{img ? (
<>
<meta key="og:image" name="og:image" content={img} />
<meta key="og:image:alt" name="og:image:alt" content={imgAlt ?? ''} />
<meta key="twitter:image" name="twitter:image" content={img} />
<meta key="twitter:image:alt" name="twitter:image:alt" content={imgAlt ?? ''} />
</>
) : null}
</Head>
);
};

View File

@ -1,40 +0,0 @@
import Box from "@/Components/Atoms/Box";
import { useBreakpoints } from "@/Contexts/BreakPointContext";
import useClient from "@/Hooks/useClient";
import { Calendar, Compass, Map, MapPin, Menu } from "lucide-react";
import { PropsWithChildren } from "react";
const AdminLayout: React.FC<PropsWithChildren> = ({ children }) => {
const { isDesktop } = useBreakpoints();
const isClient = useClient();
if (!isClient) return;
return (
<Box className="min-h-screen bg-background">
<nav className="bg-white shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<MapPin className="w-8 h-8 text-indigo-600" />
<span className="ml-2 text-2xl font-bold text-indigo-600">Wisatin - Admin</span>
</div>
<div className="hidden md:flex items-center space-x-4">
<button className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition">
🇮🇩 ID
</button>
<button className="px-3 py-2 text-gray-600 hover:text-gray-800 transition">
EN 🇺🇸
</button>
<button className="px-6 py-2 border-2 border-gray-300 rounded-lg font-medium hover:border-indigo-600 hover:text-indigo-600 transition">
Logout
</button>
</div>
</div>
</div>
</nav>
</Box>
)
}
export default AdminLayout;

View File

@ -1,99 +0,0 @@
import { Table, TableColumnsType } from 'antd';
import { TableRowSelection } from 'antd/es/table/interface';
import { TablePaginationConfig, TableProps } from 'antd/lib';
import React, { Key } from 'react';
type DataTableProps<T> = {
selectedRowKeys?: React.Key[];
columns: TableColumnsType<T>;
data: T[];
length?: number;
pagesize?: number;
selectedLength?: number;
tableLayout?: TableProps['tableLayout'];
toolbar?: React.ReactNode;
paginationPosition?: TablePaginationConfig['position'];
setSelectedRowKeys?: (keys: Key[]) => void;
onPageChange?: TableProps<T>['onChange'];
footer?: TableProps<T>['footer'];
} & Omit<TableProps<T>, 'columns'>;
// eslint-disable-next-line comma-spacing
const DataTable = <T,>({
selectedRowKeys,
columns,
data,
length,
pagesize,
selectedLength,
tableLayout = 'fixed',
toolbar,
setSelectedRowKeys,
onPageChange,
footer,
paginationPosition,
...rest
}: DataTableProps<T>) => {
const rowSelection: TableRowSelection<T> = {
selectedRowKeys,
checkStrictly: false,
onChange: (newSelectedRowKeys: React.Key[]) => {
if (!setSelectedRowKeys) return;
setSelectedRowKeys(newSelectedRowKeys);
},
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
{
key: 'odd',
text: 'Select Odd Row',
onSelect: (changeableRowKeys) => {
const newSelectedRowKeys = changeableRowKeys.filter((_, index) => {
if (index % 2 !== 0) return false;
return true;
});
if (newSelectedRowKeys && setSelectedRowKeys) setSelectedRowKeys(newSelectedRowKeys);
},
},
{
key: 'even',
text: 'Select Even Row',
onSelect: (changeableRowKeys) => {
const newSelectedRowKeys = changeableRowKeys.filter((_, index) => {
if (index % 2 !== 0) return true;
return false;
});
setSelectedRowKeys && setSelectedRowKeys(newSelectedRowKeys);
},
},
],
};
const canSelectRow = selectedRowKeys !== undefined && setSelectedRowKeys !== undefined;
return (
<>
{toolbar}
<Table<T>
rowSelection={canSelectRow ? rowSelection : undefined}
columns={columns}
dataSource={data}
onChange={onPageChange}
pagination={{
showSizeChanger: true,
pageSize: pagesize,
...(paginationPosition && { position: paginationPosition }),
}}
tableLayout={tableLayout}
footer={
footer ? undefined : () => (selectedLength ? `${selectedLength} of ${length} row(s) selected` : '')
}
className="!overflow-x-auto"
{...rest}
/>
</>
);
};
export default DataTable;

View File

@ -1,40 +0,0 @@
import Box from "@/Components/Atoms/Box";
import { useBreakpoints } from "@/Contexts/BreakPointContext";
import useClient from "@/Hooks/useClient";
import { Calendar, Compass, Map, MapPin, Menu } from "lucide-react";
import { PropsWithChildren } from "react";
const ProtectedLayout: React.FC<PropsWithChildren> = ({ children }) => {
const { isDesktop } = useBreakpoints();
const isClient = useClient();
if (!isClient) return;
return (
<Box className="min-h-screen bg-background">
<nav className="bg-white shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<MapPin className="w-8 h-8 text-indigo-600" />
<span className="ml-2 text-2xl font-bold text-indigo-600">Wisatin</span>
</div>
<div className="hidden md:flex items-center space-x-4">
<button className="px-4 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition">
🇮🇩 ID
</button>
<button className="px-3 py-2 text-gray-600 hover:text-gray-800 transition">
EN 🇺🇸
</button>
<button className="px-6 py-2 border-2 border-gray-300 rounded-lg font-medium hover:border-indigo-600 hover:text-indigo-600 transition">
Logout
</button>
</div>
</div>
</div>
</nav>
</Box>
)
}
export default ProtectedLayout;

View File

@ -1,100 +0,0 @@
import Box from "@/Components/Atoms/Box";
import { useBreakpoints } from "@/Contexts/BreakPointContext";
import useClient from "@/Hooks/useClient";
import { HugeiconsIcon } from "@hugeicons/react";
import { Button, Radio, Space } from "antd";
import { Calendar, Compass, Map, MapPin, Menu, Store } from "lucide-react";
import Link from "next/link";
import { PropsWithChildren, useState } from "react";
import { DiscoverSquareIcon, Home07Icon, UserSquareIcon } from '@hugeicons/core-free-icons'
import { usePathname } from "next/navigation";
import { cn } from "@/Functions/cn";
const PublicLayout: React.FC<PropsWithChildren> = ({ children }) => {
const { isDesktop } = useBreakpoints();
const isClient = useClient();
const pathname = usePathname()
const [position, setPosition] = useState<'ID' | 'EN'>('ID');
if (!isClient) return;
return (
<Box className="min-h-screen bg-gray-50 relative">
<nav className="bg-white shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center">
<MapPin className="w-8 h-8 text-primary" />
<span className="ml-2 text-2xl font-bold text-primary">Wisatin</span>
</div>
{/* Desktop Menu */}
<div className="items-center gap-6 flex">
<div className="hidden md:flex items-center gap-12">
<Link href="/" className={cn(
"flex items-center text-gray-700 hover:text-primary transition",
pathname === "/" && "text-primary"
)}>
<span className="font-semibold">Home</span>
</Link>
<Link href="/explore" className={cn(
"flex items-center text-gray-700 hover:text-primary transition",
pathname === "/explore" && "text-primary"
)}>
<span className="font-semibold">Explore</span>
</Link>
</div>
<div className="hidden md:flex items-center space-x-6">
<Space>
<Radio.Group
value={position}
onChange={(e) => setPosition(e.target.value)}
>
<Radio.Button value="ID">🇮🇩 ID</Radio.Button>
<Radio.Button value="EN">EN 🇺🇸</Radio.Button>
</Radio.Group>
</Space>
<Button type="default" variant="outlined">
<Link href="/login">
Login
</Link>
</Button>
</div>
</div>
</div>
</div>
</nav>
<div>
{children}
</div>
<div className="md:hidden fixed bottom-0 left-0 right-0 bg-white shadow-md">
<div className="flex items-center justify-around py-2">
<Link href="/" className={cn(
"flex flex-col items-center gap-1 text-black text-sm",
pathname === "/" && "text-primary"
)}>
<HugeiconsIcon icon={Home07Icon} className="size-7" />
<span>Home</span>
</Link>
<Link href="/explore" className={cn(
"flex flex-col items-center gap-1 text-black text-sm",
pathname === "/explore" && "text-primary"
)}>
<HugeiconsIcon icon={DiscoverSquareIcon} className="size-7" />
<span>Explore</span>
</Link>
<Link href="/login" className={cn(
"flex flex-col items-center gap-1 text-black text-sm",
pathname === "/login" && "text-primary"
)}>
<HugeiconsIcon icon={UserSquareIcon} className="size-7" />
<span>Account</span>
</Link>
</div>
</div>
</Box>
)
}
export default PublicLayout;

View File

@ -1,17 +0,0 @@
import React from 'react';
import Box from '../Atoms/Box';
import { cn } from '@/Functions/cn';
import { LayoutProps } from '@/Types/Layout';
import AdminLayout from '../Molecules/AdminLayout';
const AdminPageLayout = ({ children, className = '' }: LayoutProps) => {
return (
<Box id="admin-layout" className={cn('relative', className)}>
<AdminLayout>
{children}
</AdminLayout>
</Box>
);
};
export default AdminPageLayout;

View File

@ -1,17 +0,0 @@
import React from 'react';
import Box from '../Atoms/Box';
import { cn } from '@/Functions/cn';
import { LayoutProps } from '@/Types/Layout';
import ProtectedLayout from '../Molecules/ProtectedLayout';
const ProtectedPageLayout = ({ children, className = '' }: LayoutProps) => {
return (
<Box id="protected-layout" className={cn('relative', className)}>
<ProtectedLayout>
{children}
</ProtectedLayout>
</Box>
);
};
export default ProtectedPageLayout;

View File

@ -1,17 +0,0 @@
import React from 'react';
import Box from '../Atoms/Box';
import { cn } from '@/Functions/cn';
import { LayoutProps } from '@/Types/Layout';
import PublicLayout from '../Molecules/PublicLayout';
const PublicPageLayout = ({ children, className = '' }: LayoutProps) => {
return (
<Box id="public-layout" className={cn('relative', className)}>
<PublicLayout>
{children}
</PublicLayout>
</Box>
);
};
export default PublicPageLayout;

View File

@ -1,3 +0,0 @@
export const adminRoutes = [
'/admin/dashboard',
];

View File

@ -1,3 +0,0 @@
export const protectedRoutes = [
'/account',
];

View File

@ -1 +0,0 @@
export const publicRoutes = ['/', '/explore', '/login', 'register'];

View File

@ -1,48 +0,0 @@
import React, { createContext, useContext, useMemo, ReactNode, useEffect, useState } from 'react';
import { Grid } from 'antd';
import { BreakpointContextType } from '@/Types/BreakPoint';
const { useBreakpoint } = Grid;
const BreakpointContext = createContext<BreakpointContextType | undefined>(undefined);
export const BreakpointProvider = ({ children }: { children: ReactNode }) => {
const screens = useBreakpoint();
const value = useMemo(
() => ({
isMobile: !!screens.xs,
isTablet: !!screens.sm || !!screens.md,
isDesktop: !!screens.lg || !!screens.xl || !!screens.xxl,
}),
[screens],
);
return <BreakpointContext.Provider value={value}>{children}</BreakpointContext.Provider>;
};
export const useBreakpoints = (): BreakpointContextType => {
const context = useContext(BreakpointContext);
if (context === undefined) throw new Error('useBreakpoints must be used within a BreakpointProvider');
return context;
};
export function useScreenWidth() {
const [windowWidth, setWindowWidth] = useState<number | null>(null);
useEffect(() => {
if (typeof window === 'undefined') return;
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
handleResize();
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowWidth;
}

View File

@ -1,23 +0,0 @@
import { ConfigProvider } from 'antd';
import React from 'react';
import { ToastProvider } from './ToastContext';
import { BreakpointProvider } from './BreakPointContext';
import theme from '@/theme/themeConfig';
type GroupContextProviderProps = {
children: React.ReactNode;
};
const GroupContextProvider = ({ children }: GroupContextProviderProps) => {
return (
<ConfigProvider theme={theme}>
<ToastProvider>
<BreakpointProvider>
{children}
</BreakpointProvider>
</ToastProvider>
</ConfigProvider>
);
};
export default GroupContextProvider;

View File

@ -1,25 +0,0 @@
import { message } from 'antd';
import React, { createContext } from 'react';
type GlobalStoresType = {
showToast: typeof message | null;
};
const ToastStores: GlobalStoresType = {
showToast: null,
};
const ToastContext = createContext(ToastStores);
message.config({
top: 0,
duration: 3,
maxCount: 3,
getContainer: () => document.body,
});
const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <ToastContext.Provider value={{ showToast: message }}>{children}</ToastContext.Provider>;
};
export { ToastContext, ToastProvider };

View File

@ -1,6 +0,0 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@ -1,11 +0,0 @@
import { useEffect, useState } from 'react';
const useClient = () => {
const [clientLoaded, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
return clientLoaded;
};
export default useClient;

View File

@ -1,9 +0,0 @@
import Box from "@/Components/Atoms/Box";
export default function Dashboard(){
return (
<Box className="flex flex-col gap-6">
Dashboard
</Box>
)
}

View File

@ -1,9 +0,0 @@
import Box from "@/Components/Atoms/Box";
export default function Account(){
return (
<Box className="flex flex-col gap-6">
Account
</Box>
)
}

View File

@ -1,9 +0,0 @@
import Box from "@/Components/Atoms/Box";
export default function Explore(){
return (
<Box className="flex flex-col gap-6">
Explore
</Box>
)
}

View File

@ -1,62 +0,0 @@
import React, {
ComponentPropsWithRef,
useCallback,
useEffect,
useState
} from 'react'
import { EmblaCarouselType } from 'embla-carousel'
type UseDotButtonType = {
selectedIndex: number
scrollSnaps: number[]
onDotButtonClick: (index: number) => void
}
export const useDotButton = (
emblaApi: EmblaCarouselType | undefined
): UseDotButtonType => {
const [selectedIndex, setSelectedIndex] = useState(0)
const [scrollSnaps, setScrollSnaps] = useState<number[]>([])
const onDotButtonClick = useCallback(
(index: number) => {
if (!emblaApi) return
emblaApi.scrollTo(index)
},
[emblaApi]
)
const onInit = useCallback((emblaApi: EmblaCarouselType) => {
setScrollSnaps(emblaApi.scrollSnapList())
}, [])
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setSelectedIndex(emblaApi.selectedScrollSnap())
}, [])
useEffect(() => {
if (!emblaApi) return
onInit(emblaApi)
onSelect(emblaApi)
emblaApi.on('reInit', onInit).on('reInit', onSelect).on('select', onSelect)
}, [emblaApi, onInit, onSelect])
return {
selectedIndex,
scrollSnaps,
onDotButtonClick
}
}
type PropType = ComponentPropsWithRef<'button'>
export const DotButton: React.FC<PropType> = (props) => {
const { children, ...restProps } = props
return (
<button type="button" {...restProps}>
{children}
</button>
)
}

View File

@ -1,92 +0,0 @@
import React, {
ComponentPropsWithRef,
useCallback,
useEffect,
useState
} from 'react'
import { EmblaCarouselType } from 'embla-carousel'
type UsePrevNextButtonsType = {
prevBtnDisabled: boolean
nextBtnDisabled: boolean
onPrevButtonClick: () => void
onNextButtonClick: () => void
}
export const usePrevNextButtons = (
emblaApi: EmblaCarouselType | undefined
): UsePrevNextButtonsType => {
const [prevBtnDisabled, setPrevBtnDisabled] = useState(true)
const [nextBtnDisabled, setNextBtnDisabled] = useState(true)
const onPrevButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.scrollPrev()
}, [emblaApi])
const onNextButtonClick = useCallback(() => {
if (!emblaApi) return
emblaApi.scrollNext()
}, [emblaApi])
const onSelect = useCallback((emblaApi: EmblaCarouselType) => {
setPrevBtnDisabled(!emblaApi.canScrollPrev())
setNextBtnDisabled(!emblaApi.canScrollNext())
}, [])
useEffect(() => {
if (!emblaApi) return
onSelect(emblaApi)
emblaApi.on('reInit', onSelect).on('select', onSelect)
}, [emblaApi, onSelect])
return {
prevBtnDisabled,
nextBtnDisabled,
onPrevButtonClick,
onNextButtonClick
}
}
type PropType = ComponentPropsWithRef<'button'>
export const PrevButton: React.FC<PropType> = (props) => {
const { children, ...restProps } = props
return (
<button
className="embla__button embla__button--prev"
type="button"
{...restProps}
>
<svg className="embla__button__svg" viewBox="0 0 532 532">
<path
fill="currentColor"
d="M355.66 11.354c13.793-13.805 36.208-13.805 50.001 0 13.785 13.804 13.785 36.238 0 50.034L201.22 266l204.442 204.61c13.785 13.805 13.785 36.239 0 50.044-13.793 13.796-36.208 13.796-50.002 0a5994246.277 5994246.277 0 0 0-229.332-229.454 35.065 35.065 0 0 1-10.326-25.126c0-9.2 3.393-18.26 10.326-25.2C172.192 194.973 332.731 34.31 355.66 11.354Z"
/>
</svg>
{children}
</button>
)
}
export const NextButton: React.FC<PropType> = (props) => {
const { children, ...restProps } = props
return (
<button
className="embla__button embla__button--next"
type="button"
{...restProps}
>
<svg className="embla__button__svg" viewBox="0 0 532 532">
<path
fill="currentColor"
d="M176.34 520.646c-13.793 13.805-36.208 13.805-50.001 0-13.785-13.804-13.785-36.238 0-50.034L330.78 266 126.34 61.391c-13.785-13.805-13.785-36.239 0-50.044 13.793-13.796 36.208-13.796 50.002 0 22.928 22.947 206.395 206.507 229.332 229.454a35.065 35.065 0 0 1 10.326 25.126c0 9.2-3.393 18.26-10.326 25.2-45.865 45.901-206.404 206.564-229.332 229.52Z"
/>
</svg>
{children}
</button>
)
}

View File

@ -1,118 +0,0 @@
import { Fragment, useMemo, useRef, useState } from 'react';
import { Button } from 'antd';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import useClient from '@/Hooks/useClient';
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { useBreakpoints } from '@/Contexts/BreakPointContext';
export default function HomeBanner() {
const isClient = useClient();
const { isMobile, isTablet, isDesktop } = useBreakpoints()
console.log({isMobile, isTablet, isDesktop});
const [activeIndex, setActiveIndex] = useState<number>(0);
const sliderRef = useRef<Slider | null>(null);
const banners = [
"/images/banners/ken2025-home.png",
"/images/banners/home-wellness-2025-2.png",
"/images/banners/home-brilian.png",
"/images/banners/arti.png",
]
const onNext = () => {
sliderRef?.current?.slickNext()
}
const onPrevious = () => {
sliderRef?.current?.slickPrev()
}
if (!isClient) return;
return (
<div className='flex flex-col relative mt-5'>
<Slider
ref={sliderRef}
// className='center'
centerMode
infinite
beforeChange={(current, next) => setActiveIndex(next)}
slidesToShow={isMobile ? 1 : 3}
slidesToScroll={1}
arrows={false}
speed={500}
responsive={[
{
breakpoint: 1024,
settings: {
slidesToShow: 2,
slidesToScroll: 1,
centerMode: true
}
},
{
breakpoint: 600,
settings: {
slidesToShow: 2,
slidesToScroll: 2,
centerMode: true
}
},
{
breakpoint: 480,
settings: {
slidesToShow: 1,
slidesToScroll: 1,
centerMode: true
}
},
{
breakpoint: 300,
settings: {
slidesToShow: 1,
slidesToScroll: 1,
centerMode: true
}
}
]}
>
{banners.map((item, index) => (
<Fragment key={index}>
<div className='mx-3'>
<img
src={item}
className='w-full xl:h-[300px] lg:h-[200px] h-[150px] object-cover rounded-2xl'
/>
</div>
</Fragment>
))}
</Slider>
<div className="flex justify-center items-center gap-2 mt-2">
<Button variant='text' type='text' onClick={onPrevious}>
<ChevronLeft />
</Button>
{banners.map((_, index) => (
<button
key={index}
className={`transition-all duration-300 rounded-full ${index === activeIndex
? 'bg-primary w-8 h-3'
: 'bg-gray-300 hover:bg-gray-400 w-3 h-3'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
<Button variant='text' type='text' onClick={onNext}>
<ChevronRight />
</Button>
</div>
</div>
)
}

View File

@ -1,11 +0,0 @@
import dynamic from "next/dynamic";
import HomeBanner from "./HomeBanner";
import Box from "@/Components/Atoms/Box";
export default function Home(){
return (
<Box className="flex flex-col gap-6">
<HomeBanner/>
</Box>
)
}

View File

@ -1,9 +0,0 @@
import Box from "@/Components/Atoms/Box";
export default function Login(){
return (
<Box className="flex flex-col gap-6">
Login
</Box>
)
}

View File

@ -1,9 +0,0 @@
import Box from "@/Components/Atoms/Box";
export default function Register(){
return (
<Box className="flex flex-col gap-6">
REGISTER
</Box>
)
}

View File

@ -1,35 +0,0 @@
@import "tailwindcss";
:root {
--background: hwb(0 100% 0%);
--primary: #FF4D00;
--secondary: #1F2937;
--error: #FF6B6B;
--success: #00B894;
--info: #63B3ED;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-primary: var(--primary);
--color-secondary: var(--secondary);
--color-error: var(--error);
--color-success: var(--success);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: #0a0a0a;
font-family: Arial, Helvetica, sans-serif;
}

View File

@ -1,5 +0,0 @@
export interface BreakpointContextType {
isMobile: boolean;
isTablet: boolean;
isDesktop: boolean;
}

View File

@ -1,6 +0,0 @@
export interface SvgIconProps extends React.SVGProps<SVGSVGElement> {
width?: string;
height?: string;
fill?: string;
className?: string;
}

View File

@ -1,6 +0,0 @@
import { ReactNode } from 'react';
export interface LayoutProps {
children: ReactNode;
className?: string;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

5
src/assets/base.css Normal file
View File

@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
font-family: "Poppins", serif;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
src/assets/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

BIN
src/assets/icon-check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
src/assets/icon-pt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
src/assets/icon_check.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 929 B

BIN
src/assets/icon_law.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 B

BIN
src/assets/logo-solnus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

4
src/assets/main.css Normal file
View File

@ -0,0 +1,4 @@
@import './base.css';

BIN
src/assets/salad.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
src/assets/spaghetti.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
src/assets/steak.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

View File

@ -0,0 +1,315 @@
<template>
<header>
<nav class="navbar">
<div class="left">
<router-link to="/">
<img src="@/assets/logo-solnus.png" class="logo" />
</router-link>
</div>
<div class="right">
<ul class="nav-links" :class="{ active: isMenuOpen }">
<li><a href="#solusi" @click.prevent="scrollToSection('solusi')" :class="{ 'active-link': activeSection === 'solusi' }">Solusi</a></li>
<li class="dropdown" ref="dropdownRef">
<button type="button" class="dropdown-toggle" @click="toggleDropdown" :class="{ 'active-link': $route.path.startsWith('/packages') }">
Paket Layanan
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-left: 4px; transition: transform 0.2s;" :style="{ transform: showDropdown ? 'rotate(180deg)' : 'rotate(0deg)' }">
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="dropdown-menu" :class="{ show: showDropdown }">
<router-link to="/packages" @click="closeMenu">Semua Paket</router-link>
<router-link to="/packages/pendirian" @click="closeMenu">Paket Pendirian</router-link>
<router-link to="/packages/perubahan" @click="closeMenu">Paket Perubahan & Pembubaran</router-link>
<router-link to="/packages/property" @click="closeMenu">Paket Property</router-link>
<router-link to="/packages/khusus" @click="closeMenu">Paket Khusus</router-link>
<router-link to="/packages/umum" @click="closeMenu">Paket Umum</router-link>
</div>
</li>
<li><a href="#kontak" @click.prevent="scrollToSection('kontak')" :class="{ 'active-link': activeSection === 'kontak' }">Kontak Kami</a></li>
<li>
<a href="#konsultasi" @click.prevent="scrollToSection('konsultasi')" class="consult-btn" :class="{ 'active-link': activeSection === 'konsultasi' }">Konsultasi Gratis</a>
</li>
</ul>
<div class="hamburger" @click="toggleMenu">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
</div>
</nav>
</header>
</template>
<script>
export default {
data() {
return {
isMenuOpen: false,
activeSection: '',
showDropdown: false,
};
},
methods: {
toggleMenu() {
this.isMenuOpen = !this.isMenuOpen;
},
closeMenu() {
this.isMenuOpen = false;
this.showDropdown = false;
},
toggleDropdown() {
this.showDropdown = !this.showDropdown;
},
scrollToSection(sectionId) {
this.closeMenu();
// If not on home page, navigate to home first
if (this.$route.path !== '/') {
this.$router.push('/').then(() => {
// Wait for DOM to be fully rendered
setTimeout(() => {
this.performScroll(sectionId);
}, 100);
});
} else {
this.performScroll(sectionId);
}
},
performScroll(sectionId) {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
},
handleScroll() {
// Only track sections on home page
if (this.$route.path !== '/') {
this.activeSection = '';
return;
}
const sections = ['solusi', 'konsultasi', 'kontak'];
const scrollPosition = window.scrollY + 100;
for (const sectionId of sections) {
const element = document.getElementById(sectionId);
if (element) {
const offsetTop = element.offsetTop;
const offsetBottom = offsetTop + element.offsetHeight;
if (scrollPosition >= offsetTop && scrollPosition < offsetBottom) {
this.activeSection = sectionId;
return;
}
}
}
this.activeSection = '';
}
},
mounted() {
// Close menu when clicking on any nav link
const navLinks = this.$el.querySelectorAll('.nav-links a');
navLinks.forEach(link => {
link.addEventListener('click', () => {
this.closeMenu();
});
});
// Track scroll position for active section
window.addEventListener('scroll', this.handleScroll);
this.handleScroll();
},
beforeUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
};
</script>
<style scoped>
.navbar {
position: fixed;
top: 0;
width: 100%;
padding: 10px;
background: white;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
overflow: visible;
}
.logo {
width: 140px;
cursor: pointer;
}
.right {
display: flex;
align-items: center;
gap: 48px;
overflow: visible;
}
.nav-links {
list-style: none;
display: flex;
gap: 50px;
align-items: center;
margin-right: 20px;
overflow: visible;
}
.nav-links li a {
text-decoration: none;
color: #333;
font-size: 16px;
font-weight: 500;
}
.nav-links li a:hover {
color: #FF6640;
}
.nav-links li a.router-link-active,
.nav-links li a.router-link-exact-active,
.nav-links li a.active-link {
color: #FF6640;
}
.dropdown {
position: relative;
}
.dropdown-toggle {
display: flex;
align-items: center;
cursor: pointer;
background: none;
border: none;
color: #333;
font-size: 16px;
font-weight: 500;
padding: 0;
}
.dropdown-menu {
display: none;
position: absolute;
top: 50px;
left: 0;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 8px 0;
margin: 0;
min-width: 260px;
z-index: 99999;
}
.dropdown-menu.show {
display: block;
}
.dropdown-menu a {
display: block;
padding: 12px 20px;
color: #333;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: background 0.2s, color 0.2s;
white-space: nowrap;
}
.dropdown-menu a:hover {
background: #f9fafb;
color: #FF6640;
}
.dropdown-menu a.router-link-active,
.dropdown-menu a.router-link-exact-active {
color: #FF6640;
background: #fff5f3;
}
.consult-btn {
padding: 10px 22px;
border: 2px solid #FF6640;
border-radius: 8px;
font-weight: bold;
color: #FF6640;
white-space: nowrap; /* prevent wrapping */
}
.consult-btn:hover {
background: #FF6640;
color: white !important;
}
.hamburger {
display: none;
flex-direction: column;
gap: 5px;
cursor: pointer;
margin-left: 20px;
}
.bar {
width: 28px;
height: 3px;
background: #333;
}
/* Mobile */
@media (max-width: 768px) {
.nav-links {
display: none;
flex-direction: column;
position: absolute;
top: 70px;
right: 0;
background: white;
width: 100%;
padding: 20px 0;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
gap: 0;
}
.nav-links.active {
display: flex;
}
.hamburger {
display: flex;
}
.dropdown {
width: 100%;
}
.dropdown-toggle {
padding: 12px 20px;
width: 100%;
justify-content: space-between;
}
.dropdown-menu {
position: static;
transform: none;
box-shadow: none;
border-left: 3px solid #FF6640;
background: #f9fafb;
margin: 0;
}
.nav-links > li > a {
display: block;
padding: 12px 20px;
}
}
</style>

View File

@ -0,0 +1,46 @@
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
const showButton = ref(false);
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: "smooth" });
};
const handleScroll = () => {
showButton.value = window.scrollY > 0;
};
onMounted(() => {
window.addEventListener("scroll", handleScroll);
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
});
</script>
<template>
<button v-if="showButton" @click="scrollToTop">&#x2191;</button>
</template>
<style scoped>
button {
position: fixed;
bottom: 20px;
right: 20px;
width: 50px;
height: 50px;
background-color: #f8b400;;
color: black;
border: none;
border-radius: 50%;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
cursor: pointer;
font-size: 20px;
}
button:hover {
background-color: #c39004;;
}
</style>

View File

@ -0,0 +1,83 @@
<template>
<footer class="footer">
<div class="footer-wrapper">
<div class="container">
<div class="grid">
<div class="col brand">
<div class="logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="6" fill="#FF6640"/>
<path d="M8 10h12M8 14h12M8 18h8" stroke="#fff" stroke-width="2" stroke-linecap="round"/>
</svg>
<span class="logo-text">Solnus</span>
</div>
<p class="tagline">Urus Legalitas Bisnis Tanpa Ribet.<br/>Dari pendirian PT, CV, hingga Yayasan,<br/>semua proses hukum kini lebih cepat,<br/>aman, dan transparan bersama tim<br/>profesional berpengalaman.</p>
<div class="contact">+62 821 xxxx xxxx</div>
<div class="social">
<a href="#" aria-label="LinkedIn">
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/></svg>
</a>
<a href="#" aria-label="Instagram">
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
</a>
</div>
</div>
<div class="col">
<h3 class="title">Navigasi</h3>
<ul class="links">
<li><a href="#">Solusi</a></li>
<li><a href="#">Kenapa Solnus?</a></li>
<li><a href="#">Layanan Solnus</a></li>
<li><a href="#">Cara Kerja</a></li>
<li><a href="#">Testimoni</a></li>
</ul>
</div>
<div class="col">
<h3 class="title">Layanan</h3>
<ul class="links">
<li><a href="#">Pendirian PT (Perseroan Terbatas)</a></li>
<li><a href="#">Pendirian CV (Commanditaire Vennootschap)</a></li>
<li><a href="#">Pendirian Yayasan</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="bottom">
<div class="container">
<div class="bottom-content">
<span>© 2025 All Rights Reserved | PT Solusi Legal Nusantara</span>
<a href="#" class="privacy">Privacy Policy</a>
</div>
</div>
</div>
</footer>
</template>
<style scoped>
.footer { background: #fff; border-top: 1px solid #e5e7eb; }
.footer-wrapper { padding: 60px 20px 0; }
.container { max-width: 1200px; margin: 0 auto; }
.grid { display: grid; gap: 48px; grid-template-columns: 1fr; }
@media (min-width: 768px) { .grid { grid-template-columns: 1.5fr 1fr 1fr; } }
.col { }
.brand { }
.logo { display: flex; align-items: center; gap: 10px; margin-bottom: 16px; }
.logo-text { font-size: 24px; font-weight: 800; color: #FF6640; }
.tagline { font-size: 14px; line-height: 1.7; color: #6b7280; margin: 0 0 20px; }
.contact { font-size: 16px; font-weight: 600; color: #111827; margin-bottom: 16px; }
.social { display: flex; gap: 12px; }
.social a { width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; color: #6b7280; transition: color .2s; }
.social a:hover { color: #111827; }
.title { font-size: 16px; font-weight: 700; color: #111827; margin: 0 0 16px; }
.links { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
.links a { color: #6b7280; text-decoration: none; font-size: 14px; transition: color .2s; }
.links a:hover { color: #111827; }
.bottom { background: #111827; margin-top: 48px; padding: 20px; }
.bottom-content { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; color: #9ca3af; font-size: 14px; }
.privacy { color: #9ca3af; text-decoration: none; }
.privacy:hover { color: #fff; }
@media (max-width: 640px) { .bottom-content { flex-direction: column; text-align: center; } }
</style>

View File

@ -0,0 +1,39 @@
<script setup>
const props = defineProps({
title: { type: String, default: 'Jangan biarkan bisnis hanya Anda berdiri di atas dokumen yang lemah.' },
buttonText: { type: String, default: 'Konsultasi Gratis' }
})
</script>
<template>
<section class="cta">
<div class="overlay"></div>
<div class="container">
<h2 class="title">{{ title }}</h2>
<button class="btn">{{ buttonText }}</button>
</div>
</section>
</template>
<style scoped>
.cta {
padding: 80px 20px;
text-align: center;
position: relative;
background-image: url('@/assets/business-hero.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.65);
z-index: 1;
}
.container { max-width: 800px; margin: 0 auto; position: relative; z-index: 2; }
.title { color: #fff; font-size: 28px; font-weight: 700; line-height: 1.4; margin: 0 0 24px; }
.btn { padding: 14px 32px; background: #FF6640; color: #fff; border: none; border-radius: 6px; font-weight: 600; font-size: 16px; cursor: pointer; transition: background .2s; }
.btn:hover { background: #e85a30; }
@media (min-width: 768px) { .title { font-size: 36px; } }
</style>

View File

@ -0,0 +1,174 @@
<template>
<section class="contact">
<h2 class="contact-title">{{ heading }}</h2>
<p class="contact-description">
{{ subheading }}
</p>
<div class="contact-items">
<article
v-for="item in contactItems"
:key="item.id"
class="item"
>
<i aria-hidden="true">{{ item.icon }}</i>
<div class="details">
<h3>{{ item.title }}</h3>
<p class="detail-text">{{ item.detail }}</p>
<p
v-if="item.description"
class="detail-subtext"
>
{{ item.description }}
</p>
</div>
</article>
</div>
</section>
</template>
<script setup>
const heading = "Hubungi Solnus";
const subheading =
"Tim kami siap mendampingi legalitas bisnis Anda melalui jalur komunikasi yang paling nyaman.";
const contactItems = [
{
id: "phone",
icon: "📞",
title: "Telepon",
detail: "+62 811-2233-4455",
description: "Senin - Jumat, 09.00 - 18.00 WIB"
},
{
id: "email",
icon: "✉️",
title: "Email",
detail: "support@solnus.id",
description: "Kami membalas dalam 1x24 jam kerja"
},
{
id: "office",
icon: "📍",
title: "Kantor",
detail: "Green Office Park, BSD City",
description: "Silakan buat janji sebelum kunjungan"
}
];
</script>
<style scoped>
.contact {
padding: 80px 20px;
}
.contact-title {
text-align: center;
font-size: 2.2rem;
margin-bottom: 10px;
color: var(--color-heading);
}
.contact-description {
text-align: center;
max-width: 640px;
margin: 0 auto 40px;
color: var(--color-text);
line-height: 1.6;
}
.contact-items {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
background: var(--color-background-mute);
border-radius: 50%;
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
.detail-text {
font-weight: 600;
color: var(--color-heading);
margin: 0;
}
.detail-subtext {
margin: 0.2rem 0 0;
color: var(--color-text);
}
@media (min-width: 1024px) {
.contact-items {
gap: 0;
}
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

View File

@ -0,0 +1,209 @@
<template>
<section
class="hero"
:style="heroStyle"
>
<div class="overlay">
<div class="hero-content">
<h1 class="headline">{{ headline }}</h1>
<div class="badges">
<button
v-for="badge in badges"
:key="badge.id"
class="badge"
:class="badge.variant"
type="button"
>
<img
:src="badge.icon"
:alt="badge.alt"
class="icon"
/>
{{ badge.label }}
</button>
</div>
<p class="description">
{{ description }}
</p>
<div class="cta-buttons">
<button
v-for="button in ctaButtons"
:key="button.id"
class="cta-button"
:class="button.variant"
type="button"
>
{{ button.label }}
</button>
</div>
</div>
</div>
</section>
</template>
<script setup>
import heroBackground from "@/assets/background_hero.png";
import iconLaw from "@/assets/icon_law.png";
import iconCheck from "@/assets/icon_check.png";
const headline = "Urus Legalitas Bisnis Tanpa Ribet";
const description =
"Dari pendirian PT, CV, hingga Yayasan, semua proses hukum lebih cepat, aman, dan transparan bersama tim profesional berpengalaman Solnus secara online.";
const heroStyle = {
backgroundImage: `url(${heroBackground})`
};
const badges = [
{
id: "law-support",
label: "Didampingi Tim Hukum",
variant: "primary",
icon: iconLaw,
alt: "Ikon tim hukum"
},
{
id: "verified-partner",
label: "Legal Partner Terverifikasi",
variant: "secondary",
icon: iconCheck,
alt: "Ikon verifikasi legal"
}
];
const ctaButtons = [
{ id: "consult", label: "Jadwalkan Konsultasi Gratis", variant: "primary" },
{ id: "services", label: "Lihat Layanan Solnus", variant: "secondary" }
];
</script>
<style scoped>
.hero {
position: relative;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 0 40px;
color: white;
text-align: center;
}
.overlay {
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.hero-content {
max-width: 900px;
margin-top: -40px;
}
.headline {
font-size: 4rem;
font-weight: 700;
line-height: 1.2;
margin-bottom: 30px;
}
.badges {
display: flex;
gap: 20px;
justify-content: center;
margin-bottom: 30px;
flex-wrap: wrap;
}
.badge {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 22px;
font-weight: 600;
border-radius: 8px;
border: none;
cursor: default;
font-size: 1rem;
}
.badge.primary {
background-color: #b71c1c;
}
.badge.secondary {
background-color: #5e0000;
}
.icon {
width: 20px;
height: 20px;
}
.description {
font-size: 1.25rem;
line-height: 1.6;
margin-bottom: 40px;
}
.cta-buttons {
display: flex;
gap: 20px;
justify-content: center;
flex-wrap: wrap;
}
.cta-button {
padding: 14px 28px;
font-size: 1.1rem;
border-radius: 10px;
font-weight: 700;
cursor: pointer;
border: 2px solid transparent;
}
.cta-button.primary {
background-color: #FF6640;
color: black;
border-color: #FF6640;
}
.cta-button.primary:hover {
background-color: #FF6640;
border-color: #FF6640;
}
.cta-button.secondary {
background-color: transparent;
color: white;
border-color: white;
}
.cta-button.secondary:hover {
background-color: white;
color: #222;
}
@media (max-width: 768px) {
.headline {
font-size: 2.4rem;
}
.description {
font-size: 1.05rem;
}
.hero-content {
margin-top: 0;
}
}
</style>

View File

@ -0,0 +1,170 @@
<template>
<section id="menu" class="featured">
<h2>Popular Dishes</h2>
<div class="dishes">
<div
v-for="dish in dishes"
:key="dish.id"
class="dish"
>
<img
:src="dish.image"
width="280"
height="280"
:alt="dish.alt"
/>
<h3>{{ dish.name }}</h3>
<p>{{ dish.description }}</p>
<div class="card-actions">
<a
:href="dish.whatsappLink"
class="whatsapp-button"
target="_blank"
rel="noopener"
>
WhatsApp
</a>
<button
class="detail-button"
type="button"
>
{{ dish.detailLabel }}
</button>
</div>
</div>
</div>
</section>
</template>
<script setup>
import spaghettiImg from "@/assets/spaghetti.jpg";
import steakImg from "@/assets/steak.jpg";
import saladImg from "@/assets/salad.jpg";
const whatsappLink = "https://wa.me/1234567890";
const dishes = [
{
id: "spaghetti",
name: "Spaghetti Carbonara",
description: "Classic Italian pasta with creamy sauce and pancetta.",
image: spaghettiImg,
alt: "Spaghetti Carbonara",
whatsappLink,
detailLabel: "Detail"
},
{
id: "steak",
name: "Grilled Steak",
description: "Perfectly grilled steak served with mashed potatoes.",
image: steakImg,
alt: "Grilled Steak",
whatsappLink,
detailLabel: "Detail"
},
{
id: "salad",
name: "Caesar Salad",
description: "Fresh greens tossed in Caesar dressing.",
image: saladImg,
alt: "Caesar Salad",
whatsappLink,
detailLabel: "Detail"
}
];
</script>
<style scoped>
#menu {
box-sizing: border-box;
padding: 100px 0;
}
h2 {
text-align: center;
margin-bottom: 50px;
font-size: 30px;
}
.dishes {
display: flex;
width: 100%;
box-sizing: border-box;
padding: 0 80px;
justify-content: space-evenly;
}
.dish {
background-color: white;
width: 280px;
height: 400px;
border-radius: 20px;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 0 13px 0 rgba(0, 0, 0, 0.3);
transition: 300ms;
padding: 0 0 20px;
}
.dish:hover {
margin-top: -10px;
box-shadow: 0 0 20px 1px rgba(0, 0, 0, 1);
}
img {
border-radius: 20px 20px 0 0;
height: 220px;
object-fit: cover;
}
p {
width: 230px;
}
.card-actions {
margin-top: auto;
display: flex;
justify-content: space-between;
width: 230px;
padding-top: 10px;
}
.detail-button {
background-color: #007bff;
color: white;
border: none;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.detail-button:hover {
background-color: #0056b3;
}
.whatsapp-button {
background-color: #25d366;
color: white;
text-decoration: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 14px;
}
.whatsapp-button:hover {
background-color: #1ebe5d;
}
@media (max-width: 1024px) {
.dishes {
flex-direction: column;
align-items: center;
}
.dish {
margin-bottom: 20px;
}
}
</style>

View File

@ -0,0 +1,53 @@
<script setup>
const steps = [
{
number: '1.',
title: 'Konsultasi & Pemilihan Layanan',
description: 'Pilih layanan yang sesuai dengan kebutuhan usaha Anda.'
},
{
number: '2.',
title: 'Upload & Verifikasi Dokumen',
description: 'Kirim dokumen yang dibutuhkan, kami bantu cek kelengkapannya.'
},
{
number: '3.',
title: 'Proses oleh Tim Legal',
description: 'Dokumen Anda diurus oleh notaris & konsultan hukum terpercaya.'
},
{
number: '4.',
title: 'Dokumen Selesai & Dikirim',
description: 'Terima semua dokumen resmi dalam bentuk digital dan fisik.'
}
]
</script>
<template>
<section class="reasons">
<div class="container">
<h2 class="headline">Bagaimana Kami Membantu Anda</h2>
<div class="grid">
<article v-for="(step, i) in steps" :key="i" class="card">
<div class="number-badge">{{ step.number }}</div>
<h3 class="card-title">{{ step.title }}</h3>
<p class="card-desc">{{ step.description }}</p>
</article>
</div>
</div>
</section>
</template>
<style scoped>
.reasons { padding: 80px 20px; background: #f9fafb; }
.container { max-width: 1200px; margin: 0 auto; }
.headline { font-size: 36px; font-weight: 800; text-align: center; margin: 0 0 56px; color: #111827; }
.grid { display: grid; gap: 24px; grid-template-columns: 1fr; }
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .grid { grid-template-columns: repeat(4, 1fr); } }
.card { background: #fff; padding: 32px 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); text-align: left; }
.number-badge { width: 48px; height: 48px; border-radius: 50%; border: 2px solid #e5e7eb; display: flex; align-items: center; justify-content: center; font-size: 18px; font-weight: 700; color: #6b7280; margin-bottom: 16px; }
.card-title { font-size: 18px; font-weight: 700; margin: 0 0 12px; color: #111827; line-height: 1.4; }
.card-desc { font-size: 14px; line-height: 1.6; color: #6b7280; margin: 0; }
@media (min-width: 768px) { .headline { font-size: 40px; } }
</style>

View File

@ -0,0 +1,192 @@
<template>
<div class="service-card">
<div class="diamond-wrapper">
<div class="diamond-bg"></div>
<img
:src="image"
:alt="title"
class="service-image"
/>
<div class="icon-badge">
<svg v-if="!primary" width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 3H5C3.89543 3 3 3.89543 3 5V19C3 20.1046 3.89543 21 5 21H19C20.1046 21 21 20.1046 21 19V5C21 3.89543 20.1046 3 19 3Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 9H15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M9 13H15" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M9 17H13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V15Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<!-- Tags -->
<div v-if="tags && tags.length > 0" class="tags">
<div v-for="(tag, i) in tags" :key="i" class="tag">
{{ tag }}
</div>
</div>
</div>
<h3 class="service-title">{{ title }}</h3>
<p class="service-desc">
{{ description }}
</p>
<button
class="service-btn"
:class="{ primary }"
type="button"
>
{{ buttonText }}
</button>
</div>
</template>
<script setup>
const { image, title, description, buttonText, primary, tags } = defineProps({
image: { type: String, required: true },
title: { type: String, required: true },
description: { type: String, required: true },
buttonText: { type: String, required: true },
primary: { type: Boolean, default: false },
tags: { type: Array, default: () => [] }
});
</script>
<style scoped>
.service-card {
background: #f9fafb;
padding: 32px 24px;
text-align: center;
transition: 0.2s ease-in-out;
cursor: default;
display: flex;
flex-direction: column;
align-items: center;
}
.service-card:hover {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
/* Diamond wrapper */
.diamond-wrapper {
position: relative;
width: 260px;
height: 240px;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.diamond-bg {
position: absolute;
width: 200px;
height: 200px;
background: linear-gradient(135deg, #FF6640 0%, #e85a30 100%);
transform: rotate(45deg);
z-index: 1;
border-radius: 0;
}
.service-image {
position: relative;
width: 180px;
height: 200px;
object-fit: cover;
object-position: center top;
z-index: 2;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.icon-badge {
position: absolute;
top: 15px;
right: 20px;
width: 48px;
height: 48px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 12px rgba(0,0,0,0.15);
z-index: 3;
color: #FF6640;
}
/* Tags */
.tags {
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 6px;
z-index: 3;
flex-wrap: wrap;
justify-content: center;
width: 100%;
max-width: 220px;
}
.tag {
background: rgba(75, 85, 99, 0.88);
color: #fff;
padding: 7px 14px;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0,0,0,0.25);
line-height: 1.2;
}
/* Title */
.service-title {
font-size: 22px;
font-weight: 700;
margin-bottom: 12px;
color: #111827;
}
/* Description */
.service-desc {
font-size: 14px;
color: #6b7280;
line-height: 1.6;
margin-bottom: 24px;
flex: 1;
}
/* Button */
.service-btn {
border: 2px solid #FF6640;
padding: 12px 28px;
color: #FF6640;
font-weight: 600;
background: white;
cursor: pointer;
transition: 0.2s;
font-size: 14px;
}
.service-btn:hover {
background: #FF6640;
color: white;
}
/* Primary style */
.service-btn.primary {
background: #FF6640;
color: white;
border-color: #FF6640;
}
.service-btn.primary:hover {
background: #e85a30;
border-color: #e85a30;
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<section class="services-wrapper">
<div class="container">
<div class="eyebrow">SOLUSI LEGAL</div>
<h2 class="headline">Semua Urusan Legal Anda,<br/>Kami Bantu Selesaikan</h2>
<p class="description">
Solnus membantu individu dan perusahaan mengurusberbagai kebutuhan hukum, dari pendirian badan usaha, pengurusan izin, hingga konsultasi legal. Kami menghadirkan kemudahan, transparansi, dan kepastian dalam setiap proses legal yang Anda butuhkan.
</p>
<div class="services-container">
<Service
v-for="service in services"
:key="service.id"
:image="service.image"
:title="service.title"
:description="service.description"
:button-text="service.buttonText"
:primary="service.primary"
:tags="service.tags"
/>
</div>
</div>
</section>
</template>
<script setup>
import Service from "./Service.vue";
const services = [
{
id: "business-formation",
image: 'https://images.unsplash.com/photo-1560250097-0b93528c311a?w=400&h=400&fit=crop',
title: "Pendirian Badan Usaha",
description: "Buat PT, CV, atau Yayasan secara online, dengan dokumen resmi Kemenkumham.",
buttonText: "Lihat Detail Layanan",
primary: false,
tags: ['PT', 'CV', 'Yayasan']
},
{
id: "license-nib",
image: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?w=400&h=400&fit=crop',
title: "Perizinan & NIB",
description: "Dapatkan Nomor Induk Berusaha (NIB) dan izin OSS dengan cepat & aman.",
buttonText: "Lihat Detail Layanan",
primary: false,
tags: []
},
{
id: "legal-consultation",
image: 'https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?w=400&h=400&fit=crop',
title: "Konsultasi Hukum",
description: "Konsultasikan masalah hukum bisnis Anda dengan ahli berpengalaman.",
buttonText: "Jadwalkan Konsultasi Gratis",
primary: true,
tags: ['Lebih baik PT atau CV ya?', 'Ini masih bisa daftar NIB?']
}
];
</script>
<style scoped>
.services-wrapper {
padding: 80px 20px;
background: #fff;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.eyebrow {
text-align: center;
font-size: 14px;
font-weight: 700;
color: #FF6640;
letter-spacing: 1px;
margin-bottom: 16px;
}
.headline {
font-size: 36px;
font-weight: 800;
text-align: center;
color: #111827;
margin: 0 0 20px;
line-height: 1.3;
}
.description {
font-size: 15px;
line-height: 1.7;
color: #6b7280;
text-align: center;
max-width: 800px;
margin: 0 auto 56px;
}
.services-container {
display: grid;
gap: 32px;
grid-template-columns: 1fr;
}
@media (min-width: 768px) {
.services-container {
grid-template-columns: repeat(3, 1fr);
}
.headline {
font-size: 42px;
}
}
@media (max-width: 767px) {
.headline {
font-size: 28px;
}
}
</style>

View File

@ -0,0 +1,159 @@
<script setup>
const services = [
{
id: 'pendirian',
title: 'Paket Pendirian',
icon: '🏢',
image: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=400&h=300&fit=crop'
},
{
id: 'perubahan',
title: 'Paket Perubahan\n& Pembubaran',
icon: '📄',
image: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?w=400&h=300&fit=crop'
},
{
id: 'property',
title: 'Paket Property',
icon: '🏠',
image: 'https://images.unsplash.com/photo-1560518883-ce09059eeffa?w=400&h=300&fit=crop'
},
{
id: 'khusus',
title: 'Paket Khusus',
icon: '⚖️',
image: 'https://images.unsplash.com/photo-1589829545856-d10d557cf95f?w=400&h=300&fit=crop'
},
{
id: 'umum',
title: 'Paket Umum',
icon: '🔨',
image: 'https://images.unsplash.com/photo-1505664194779-8beaceb93744?w=400&h=300&fit=crop'
}
]
</script>
<template>
<section class="services-menu">
<div class="overlay"></div>
<div class="container">
<h2 class="headline">Layanan Legal Solnus</h2>
<div class="cards">
<a
v-for="service in services"
:key="service.id"
:href="`#${service.id}`"
class="card"
>
<div class="card-icon">{{ service.icon }}</div>
<h3 class="card-title">{{ service.title }}</h3>
</a>
</div>
</div>
</section>
</template>
<style scoped>
.services-menu {
position: relative;
padding: 80px 20px;
background-image: url('@/assets/service_legal.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
.overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.75);
z-index: 1;
}
.container {
position: relative;
z-index: 2;
max-width: 1200px;
margin: 0 auto;
}
.headline {
font-size: 36px;
font-weight: 800;
text-align: center;
margin: 0 0 48px;
color: #fff;
}
.cards {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
max-width: 1100px;
margin: 0 auto;
}
@media (min-width: 640px) {
.cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 768px) {
.cards {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1024px) {
.cards {
grid-template-columns: repeat(5, 1fr);
}
}
.card {
background: #fff;
border-radius: 12px;
padding: 32px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
text-decoration: none;
transition: transform .2s, box-shadow .2s;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
}
.card-icon {
font-size: 48px;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background: #f9fafb;
border-radius: 12px;
}
.card-title {
margin: 0;
font-size: 16px;
font-weight: 700;
color: #111827;
text-align: center;
line-height: 1.4;
white-space: pre-line;
}
@media (min-width: 768px) {
.headline {
font-size: 42px;
}
}
</style>

View File

@ -0,0 +1,92 @@
<script setup>
import { ref } from 'vue'
const testimonials = [
{
name: 'Imam Santosa',
role: 'Owner Nona Craft Studio',
text: 'Saya sempat ragu bikin PT karena takut ribet dan mahal. Tapi ternyata lewat Solnus, semua prosesnya transparan, cepat, dan saya bisa pantau dari HP. Dalam 7 hari, SK dan NIB saya sudah keluar. Sekarang bisnis saya bisa kerja sama dengan brand besar.',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&h=200&fit=crop'
}
]
const current = ref(0)
function prev() {
current.value = (current.value - 1 + testimonials.length) % testimonials.length
}
function next() {
current.value = (current.value + 1) % testimonials.length
}
</script>
<template>
<section class="testimonial">
<div class="container">
<div class="eyebrow">1500+ Pengusaha Telah Buktikan</div>
<h2 class="headline">Sekarang Giliran Anda!</h2>
<div class="card">
<div class="author">
<img :src="testimonials[current].avatar" :alt="testimonials[current].name" class="avatar" />
<div class="meta">
<div class="name">{{ testimonials[current].name }}</div>
<div class="role">{{ testimonials[current].role }}</div>
</div>
</div>
<blockquote class="quote">
"{{ testimonials[current].text }}"
</blockquote>
<div class="stats">
<div class="stat">
<div class="stat-value">100%</div>
<div class="stat-label">Kepuasan pelanggan</div>
</div>
<div class="stat">
<div class="stat-value">4x</div>
<div class="stat-label">Lebih cepat dari proses manual</div>
</div>
</div>
<div class="nav">
<button @click="prev" class="nav-btn" aria-label="Previous">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 15L7.5 10L12.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button @click="next" class="nav-btn" aria-label="Next">
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.5 15L12.5 10L7.5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
</div>
</div>
</section>
</template>
<style scoped>
.testimonial { padding: 80px 20px; background: #fff; }
.container { max-width: 1200px; margin: 0 auto; }
.eyebrow { font-size: 16px; font-weight: 600; color: #FF6640; text-align: center; margin: 0 0 12px; letter-spacing: .5px; }
.headline { font-size: 40px; font-weight: 800; text-align: center; margin: 0 0 56px; color: #111827; line-height: 1.2; }
.card { padding: 48px; background: #f9fafb; border-radius: 16px; position: relative; max-width: 1000px; margin: 0 auto; }
.author { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
.avatar { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; }
.meta { display: flex; flex-direction: column; gap: 4px; }
.name { font-weight: 700; font-size: 18px; color: #111827; }
.role { font-size: 15px; color: #9ca3af; font-style: italic; }
.quote { margin: 0 0 32px; font-size: 20px; line-height: 1.7; color: #374151; font-weight: 400; }
.stats { display: flex; gap: 48px; margin-bottom: 0; }
.stat { }
.stat-value { font-size: 28px; font-weight: 800; color: #111827; margin-bottom: 4px; }
.stat-label { font-size: 14px; color: #6b7280; }
.nav { position: absolute; bottom: 48px; right: 48px; display: flex; gap: 12px; }
.nav-btn { width: 44px; height: 44px; border-radius: 50%; border: 1px solid #e5e7eb; background: #fff; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all .2s; color: #6b7280; }
.nav-btn:hover { background: #f3f4f6; border-color: #d1d5db; color: #111827; }
@media (min-width: 768px) { .headline { font-size: 48px; } }
@media (max-width: 768px) { .card { padding: 32px 24px; } .nav { position: static; margin-top: 24px; justify-content: center; } .stats { flex-direction: column; gap: 20px; } }
</style>

View File

@ -0,0 +1,67 @@
<script setup>
const features = [
{
icon: 'lightning',
title: 'Proses Cepat & Efisien',
description: 'Semua layanan dilakukan online dengan deadline jelas untuk efisiensi.'
},
{
icon: 'lock',
title: 'Aman & Terpercaya',
description: 'Dokumen diverifikasi langsung oleh notaris dan instansi resmi.'
},
{
icon: 'star',
title: 'Tim Profesional',
description: 'Didukung pengacara dan konsultan hukum berpengalaman.'
},
{
icon: 'message',
title: 'Proses Transparan',
description: 'Anda bisa pantau status dokumen kapan pun.'
}
]
</script>
<template>
<section class="why-choose">
<div class="container">
<h2 class="headline">Kenapa Bisnis Memilih Solusi Kami</h2>
<div class="grid">
<article v-for="(feature, i) in features" :key="i" class="card">
<div class="icon-wrapper">
<svg v-if="feature.icon === 'lightning'" width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 2L3 14H12L11 22L21 10H12L13 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="feature.icon === 'lock'" width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="11" width="18" height="11" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M7 11V7C7 4.79086 8.79086 3 11 3H13C15.2091 3 17 4.79086 17 7V11" stroke="currentColor" stroke-width="2"/>
</svg>
<svg v-else-if="feature.icon === 'star'" width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg v-else-if="feature.icon === 'message'" width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 11.5C21.0034 12.8199 20.6951 14.1219 20.1 15.3C19.3944 16.7118 18.3098 17.8992 16.9674 18.7293C15.6251 19.5594 14.0782 19.9994 12.5 20C11.1801 20.0035 9.87812 19.6951 8.7 19.1L3 21L4.9 15.3C4.30493 14.1219 3.99656 12.8199 4 11.5C4.00061 9.92179 4.44061 8.37488 5.27072 7.03258C6.10083 5.69028 7.28825 4.6056 8.7 3.90003C9.87812 3.30496 11.1801 2.99659 12.5 3.00003H13C15.0843 3.11502 17.053 3.99479 18.5291 5.47089C20.0052 6.94699 20.885 8.91568 21 11V11.5Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="card-title">{{ feature.title }}</h3>
<p class="card-desc">{{ feature.description }}</p>
</article>
</div>
</div>
</section>
</template>
<style scoped>
.why-choose { padding: 80px 20px; background: #111827; }
.container { max-width: 1200px; margin: 0 auto; }
.headline { font-size: 36px; font-weight: 800; text-align: center; margin: 0 0 56px; color: #fff; line-height: 1.2; }
.grid { display: grid; gap: 32px; grid-template-columns: 1fr; }
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .grid { grid-template-columns: repeat(4, 1fr); } }
.card { text-align: center; }
.icon-wrapper { width: 64px; height: 64px; margin: 0 auto 20px; display: flex; align-items: center; justify-content: center; border-radius: 50%; border: 2px solid rgba(255,255,255,0.2); color: #fff; }
.card-title { font-size: 18px; font-weight: 700; margin: 0 0 12px; color: #fff; }
.card-desc { font-size: 14px; line-height: 1.6; color: #9ca3af; margin: 0; }
@media (min-width: 768px) { .headline { font-size: 40px; } }
</style>

View File

@ -0,0 +1,60 @@
<script setup>
const props = defineProps({
title: { type: String, required: true },
price: { type: String, required: true },
subtitle: { type: String, default: '' },
image: { type: String, default: '' },
features: { type: Array, default: () => [] },
ctaText: { type: String, default: 'Jadwalkan Konsultasi Gratis' }
})
</script>
<template>
<article class="card">
<div class="media" v-if="image">
<img :src="image" :alt="title" />
<div class="overlay"></div>
<div class="media-text">
<h3>{{ title }}</h3>
<p>{{ subtitle }}</p>
</div>
</div>
<div class="body">
<div class="price">{{ price }}</div>
<ul class="features">
<li v-for="(f, i) in features" :key="i">
<svg class="icon" width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8" fill="#10b981"/>
<path d="M11 6L7 10L5 8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>{{ f }}</span>
</li>
</ul>
<button class="cta">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 6px;">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413Z" fill="currentColor"/>
</svg>
{{ ctaText }}
</button>
</div>
</article>
</template>
<style scoped>
.card { border: 1px solid #e5e7eb; border-radius: 12px; overflow: hidden; background: #fff; display: flex; flex-direction: column; height: 100%; box-shadow: 0 1px 3px rgba(0,0,0,.08); transition: box-shadow .2s; }
.card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.12); }
.media { position: relative; height: 160px; overflow: hidden; }
.media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.overlay { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0.3) 100%); }
.media-text { position: absolute; bottom: 12px; left: 16px; right: 16px; color: #fff; }
.media-text h3 { margin: 0; font-size: 18px; font-weight: 700; }
.media-text p { margin: 4px 0 0; opacity: 0.95; font-size: 13px; }
.body { padding: 20px; display: flex; flex-direction: column; gap: 16px; flex: 1; }
.price { font-weight: 800; color: #EE3000; font-size: 24px; }
.features { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.features li { display: flex; align-items: flex-start; gap: 10px; color: #374151; font-size: 14px; line-height: 1.5; }
.icon { flex-shrink: 0; margin-top: 2px; }
.cta { width: 100%; padding: 12px 16px; background: #111827; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 14px; display: flex; align-items: center; justify-content: center; transition: background .2s; }
.cta:hover { background: #000; }
</style>

View File

@ -0,0 +1,29 @@
<script setup>
import PackageCard from './PackageCard.vue'
const props = defineProps({
items: { type: Array, default: () => [] }
})
</script>
<template>
<div class="grid">
<PackageCard
v-for="(p, i) in items"
:key="i"
:title="p.title"
:subtitle="p.subtitle"
:price="p.price"
:image="p.image"
:features="p.features"
:cta-text="p.ctaText"
/>
</div>
</template>
<style scoped>
.grid { display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; }
@media (min-width: 0px) { .grid { grid-template-columns: 1fr; } }
@media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .grid { grid-template-columns: repeat(3, 1fr); } }
</style>

View File

@ -0,0 +1,44 @@
<script setup>
import { TABS as tabs, DATA_BY_TAB as dataByTab } from './data.js'
import { computed } from 'vue'
import PackagesGrid from './PackagesGrid.vue'
const props = defineProps({
initialCategory: { type: String, default: null }
})
// If no category specified, show all packages
const currentCategory = computed(() => props.initialCategory || null)
const currentData = computed(() => {
if (!currentCategory.value) {
// Show all packages from all categories
return Object.values(dataByTab).flat()
}
return dataByTab[currentCategory.value] || []
})
const headline = computed(() => {
if (!currentCategory.value) {
return 'Semua Paket Layanan'
}
const tab = tabs.find(t => t.key === currentCategory.value)
return tab ? tab.label : 'Paket Layanan'
})
</script>
<template>
<section class="packages">
<div class="container">
<h2 class="headline">{{ headline }}</h2>
<PackagesGrid :items="currentData" />
</div>
</section>
</template>
<style scoped>
.packages { padding: 80px 0; background: #fff; margin-top: 60px; }
.container { max-width: 1200px; margin: 0 auto; padding: 0 20px; }
.headline { font-size: 32px; font-weight: 800; text-align: center; margin: 0 0 40px; color: #111827; }
@media (min-width: 768px) { .headline { font-size: 40px; margin-bottom: 48px; } }
</style>

View File

@ -0,0 +1,169 @@
export const TABS = [
{ key: 'pendirian', label: 'Paket Pendirian' },
{ key: 'perubahan', label: 'Paket Perubahan & Pembubaran' },
{ key: 'property', label: 'Paket Property' },
{ key: 'khusus', label: 'Paket Khusus' },
{ key: 'umum', label: 'Paket Umum' }
]
const commonFeatures = [
'Akta Pendirian',
'SK Pendirian AHU (Kemenkum)',
'NIB',
'Akun OSS',
'K3L',
'SPPL',
'Pernyataan Mandiri',
'Sertifikat Standar (untuk KBLI tertentu)',
'NPWP',
'Akun Coretax',
'SKT Pajak',
'Surat Penerbitan Akun Pajak',
'Bukti Penerimaan Surat Pendaftaran Wajib Pajak',
'Email Badan',
'Stempel/Cap Badan',
'Free Pengecekan Nama',
'Free Konsultasi'
]
const img = (q) => `https://images.unsplash.com/${q}&auto=format&fit=crop&w=1200&q=60`
export const DATA_BY_TAB = {
pendirian: [
{
title: 'Pendirian PT',
subtitle: 'Mulai Langkah Pertama Bisnis Resmi Anda',
price: 'Rp 3,850 K',
image: img('photo-1520975916090-3105956dac38?ixlib=rb-4.0.3'),
features: commonFeatures
},
{
title: 'Paket Level Up',
subtitle: 'Saatnya Naik Kelas & Siap Dapat Klien Besar',
price: 'Rp 3,200 K',
image: img('photo-1517245386807-bb43f82c33c4?ixlib=rb-4.0.3'),
features: commonFeatures
},
{
title: 'Pendirian Yayasan',
subtitle: 'Yayasan Resmi, Siap Operasional Penuh',
price: 'Rp 3,800 K',
image: img('photo-1515165562835-c3b8b2c9288b?ixlib=rb-4.0.3'),
features: commonFeatures
},
{
title: 'Pendirian Firma',
subtitle: 'Mulai Langkah Pertama Bisnis Resmi Anda',
price: 'Rp 3,500 K',
image: img('photo-1525182008055-f88b95ff7980?ixlib=rb-4.0.3'),
features: commonFeatures
},
{
title: 'Pendirian Perkumpulan',
subtitle: 'Mulai Langkah Pertama Bisnis Resmi Anda',
price: 'Rp 5,000 K',
image: img('photo-1521737604893-d14cc237f11d?ixlib=rb-4.0.3'),
features: commonFeatures
},
{
title: 'Pendirian Persekutuan Perdata',
subtitle: 'Mulai Langkah Pertama Bisnis Resmi Anda',
price: 'Rp 3,500 K',
image: img('photo-1538688423619-a81d3f23454b?ixlib=rb-4.0.3'),
features: commonFeatures
},
{
title: 'Pendirian Koperasi',
subtitle: 'Mulai Langkah Pertama Bisnis Resmi Anda',
price: 'Rp 8,500 K',
image: img('photo-1520974722070-6a669e3f0de7?ixlib=rb-4.0.3'),
features: commonFeatures
},
{
title: 'Pendirian PT Perseorangan',
subtitle: 'Mulai Langkah Pertama Bisnis Resmi Anda',
price: 'Rp 1,200 K',
image: img('photo-1519340241574-2cec6aef0c01?ixlib=rb-4.0.3'),
features: [
'Sertifikat Pendaftaran Pendirian Perseorangan (Kemenkum)',
'Pernyataan Pendirian Kemenkum',
'NIB', 'Akun OSS', 'K3L', 'SPPL', 'Pernyataan Mandiri',
'Sertifikat Standar (untuk KBLI tertentu)', 'NPWP', 'Akun Coretax', 'SKT Pajak',
'Surat Penerbitan Akun Pajak', 'Bukti Penerimaan Surat Pendaftaran Wajib Pajak',
'Email Badan', 'Stempel/Cap Badan', 'Free Pengecekan Nama', 'Free Konsultasi'
]
}
],
perubahan: [
{
title: 'Perubahan Data PT',
subtitle: 'Ubah nama, alamat, modal, pengurus',
price: 'Konsultasi Dulu',
image: img('photo-1522075469751-3a6694fb2f61?ixlib=rb-4.0.3'),
features: [
'Analisis kebutuhan perubahan',
'Penyusunan akta perubahan',
'Pengesahan AHU (jika perlu)',
'Update OSS dan perpajakan',
'Dokumentasi lengkap',
'Free Konsultasi'
]
},
{
title: 'Pembubaran Badan Usaha',
subtitle: 'Aman sesuai regulasi',
price: 'Konsultasi Dulu',
image: img('photo-1521791136064-7986c2920216?ixlib=rb-4.0.3'),
features: [
'Konsultasi dan strategi pembubaran',
'Penyusunan akta pembubaran',
'Pencabutan perizinan terkait',
'Penutupan perpajakan (jika perlu)',
'Dokumentasi lengkap',
'Free Konsultasi'
]
}
],
property: [
{
title: 'Legalitas Property Dasar',
subtitle: 'Cek sertifikat dan dokumen',
price: 'Konsultasi Dulu',
image: img('photo-1505693416388-ac5ce068fe85?ixlib=rb-4.0.3'),
features: [
'Review dokumen kepemilikan',
'Cek keabsahan sertifikat',
'Rekomendasi langkah lanjutan',
'Free Konsultasi'
]
}
],
khusus: [
{
title: 'Paket Khusus Tender',
subtitle: 'Lengkap untuk kebutuhan pengadaan',
price: 'Customize',
image: img('photo-1542744173-8e7e53415bb0?ixlib=rb-4.0.3'),
features: [
'Dokumen administrasi lengkap',
'Perizinan spesifik KBLI',
'Pendampingan kelengkapan dokumen',
'Free Konsultasi'
]
}
],
umum: [
{
title: 'Virtual Office + Legal',
subtitle: 'Hemat dan fleksibel',
price: 'Start from Rp 1 Jt',
image: img('photo-1487014679447-9f8336841d58?ixlib=rb-4.0.3'),
features: [
'Alamat legal bisnis',
'Forwarding surat',
'Konsultasi legal dasar',
'Free Konsultasi'
]
}
]
}

View File

@ -0,0 +1,72 @@
<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({
tabs: { type: Array, required: true },
modelValue: { type: String, default: '' },
ariaLabel: { type: String, default: 'Sections' }
})
const emit = defineEmits(['update:modelValue', 'change'])
const internal = ref(props.modelValue || (props.tabs[0] && props.tabs[0].key) || '')
watch(() => props.modelValue, (v) => {
if (v && v !== internal.value) internal.value = v
})
watch(internal, (v) => {
emit('update:modelValue', v)
emit('change', v)
})
const activeIndex = computed(() => props.tabs.findIndex(t => t.key === internal.value))
function selectByIndex(idx) {
if (idx < 0 || idx >= props.tabs.length) return
internal.value = props.tabs[idx].key
}
function onKeydown(e) {
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(e.key)) return
e.preventDefault()
if (e.key === 'ArrowLeft') selectByIndex((activeIndex.value - 1 + props.tabs.length) % props.tabs.length)
if (e.key === 'ArrowRight') selectByIndex((activeIndex.value + 1) % props.tabs.length)
if (e.key === 'Home') selectByIndex(0)
if (e.key === 'End') selectByIndex(props.tabs.length - 1)
}
</script>
<template>
<div class="tabs">
<div class="tablist" role="tablist" :aria-label="ariaLabel" @keydown="onKeydown">
<button
v-for="(t, i) in tabs"
:key="t.key"
class="tab"
:class="{ active: t.key === internal }"
role="tab"
:id="`tab-${t.key}`"
:aria-selected="t.key === internal"
:tabindex="t.key === internal ? 0 : -1"
@click="internal = t.key"
>
{{ t.label }}
</button>
</div>
<div class="tabpanel">
<slot :active="internal"></slot>
</div>
</div>
</template>
<style scoped>
.tabs { width: 100%; }
.tablist { display: flex; justify-content: center; gap: 32px; border-bottom: 1px solid #e5e7eb; overflow-x: auto; }
.tab { appearance: none; background: transparent; border: none; padding: 14px 4px; cursor: pointer; color: #6b7280; font-weight: 600; font-size: 14px; letter-spacing: .25px; white-space: nowrap; border-bottom: 3px solid transparent; margin-bottom: -1px; transition: color .18s, border-color .18s; }
.tab.active { color: #111827; border-bottom-color: #FF6640; }
.tab:hover { color: #111827; }
.tabpanel { padding-top: 24px; }
</style>

7
src/main.js Normal file
View File

@ -0,0 +1,7 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')

25
src/pages/Home.vue Normal file
View File

@ -0,0 +1,25 @@
<script setup>
import Hero from "@/components/main/Hero.vue";
import ServicesMenu from "@/components/main/ServicesMenu.vue";
import ServicesSection from "@/components/main/ServiceSection.vue";
import WhyChoose from "@/components/main/WhyChoose.vue";
import CallToAction from "@/components/main/CallToAction.vue";
import ReasonsSection from "@/components/main/ReasonsSection.vue";
import Testimonial from "@/components/main/Testimonial.vue";
</script>
<template>
<main>
<Hero />
<ServicesMenu id="solusi" />
<!-- FIX: use section, not single card -->
<ServicesSection />
<WhyChoose />
<CallToAction id="konsultasi" />
<ReasonsSection />
<Testimonial />
</main>
</template>
<style scoped>
</style>

17
src/pages/Packages.vue Normal file
View File

@ -0,0 +1,17 @@
<script setup>
import PackagesSection from "../components/packages/PackagesSection.vue";
import { useRoute } from 'vue-router';
import { computed } from 'vue';
const route = useRoute();
const category = computed(() => route.meta.category || null);
</script>
<template>
<main>
<PackagesSection id="paket" :initialCategory="category" />
</main>
</template>
<style scoped>
</style>

View File

@ -1,67 +0,0 @@
import React from 'react';
import { ConfigProvider } from 'antd';
import type { AppProps } from 'next/app';
import theme from '@/theme/themeConfig';
import { Poppins } from 'next/font/google';
import Head from 'next/head';
import GroupContextProvider from '@/Contexts/GroupContextProvider';
import { useRouter } from 'next/router';
import { publicRoutes } from '@/Constants/PublicRoutes';
import PublicPageLayout from '@/Components/Organisms/PublicPageLayout';
import { adminRoutes } from '@/Constants/AdminRoutes';
import AdminPageLayout from '@/Components/Organisms/AdminPageLayout';
import ProtectedPageLayout from '@/Components/Organisms/ProtectedPageLayout';
import '@/Styles/globals.css';
const poppins = Poppins({
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
variable: '--font-poppins',
});
const AppLayout = ({ Component, ...pageProps }: AppProps) => {
const router = useRouter();
if (publicRoutes.includes(router.pathname))
return (
<PublicPageLayout className={poppins.variable}>
<Component {...pageProps} />
</PublicPageLayout>
);
else if(adminRoutes.includes(router.pathname))
<AdminPageLayout className={poppins.variable}>
<Component {...pageProps} />
</AdminPageLayout>
else
return (
<ProtectedPageLayout className={poppins.variable}>
<Component {...pageProps} />
</ProtectedPageLayout>
);
};
const App = ({ Component, pageProps }: AppProps) => {
return (
<>
<Head>
<title>Wisatin</title>
<meta charSet="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Generated by Wisatin" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Wisatin" />
<meta name="og:description" content="Generated by Wisatin" />
<meta name="og:image:alt" content="Wisatin logo" />
<meta name="twitter:image:alt" content="Wisatin logo" />
<meta name="twitter:description" content="Generated by Wisatin" />
</Head>
<GroupContextProvider>
<AppLayout Component={Component} {...pageProps} />
</GroupContextProvider>
</>
);
};
export default App;

View File

@ -1,43 +0,0 @@
import React from 'react';
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import Document, { Head, Html, Main, NextScript } from 'next/document';
import type { DocumentContext } from 'next/document';
const MyDocument = () => {
return (
<Html lang="en">
<Head />
<body className="antialiased">
<Main />
<NextScript />
</body>
</Html>
);
}
MyDocument.getInitialProps = async (ctx: DocumentContext) => {
const cache = createCache();
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => (
<StyleProvider cache={cache}>
<App {...props} />
</StyleProvider>
),
});
const initialProps = await Document.getInitialProps(ctx);
const style = extractStyle(cache, true);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style dangerouslySetInnerHTML={{ __html: style }} />
</>
),
};
};
export default MyDocument;

View File

@ -1,11 +0,0 @@
import { SEO } from '@/Components/Atoms/SEO';
import Account from '@/LegacyPages/Public/Account';
export default function Page() {
return (
<>
<SEO title="Wisatin" description="Generated by Wisatin" />
<Account />
</>
);
}

View File

@ -1,11 +0,0 @@
import { SEO } from '@/Components/Atoms/SEO';
import Dashboard from '@/LegacyPages/Admin/Dashboard';
export default function Page() {
return (
<>
<SEO title="Wisatin" description="Generated by Wisatin" />
<Dashboard />
</>
);
}

View File

@ -1,13 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from "next";
type Data = {
name: string;
};
export default function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
res.status(200).json({ name: "John Doe" });
}

View File

@ -1,11 +0,0 @@
import { SEO } from '@/Components/Atoms/SEO';
import Explore from '@/LegacyPages/Public/Explore';
export default function Page() {
return (
<>
<SEO title="Wisatin" description="Generated by Wisatin" />
<Explore />
</>
);
}

View File

@ -1,11 +0,0 @@
import { SEO } from '@/Components/Atoms/SEO';
import Home from '@/LegacyPages/Public/Home';
export default function Page() {
return (
<>
<SEO title="Wisatin" description="Generated by Wisatin" />
<Home />
</>
);
}

View File

@ -1,11 +0,0 @@
import { SEO } from '@/Components/Atoms/SEO';
import Login from '@/LegacyPages/Public/Login';
export default function Page() {
return (
<>
<SEO title="Wisatin" description="Generated by Wisatin" />
<Login />
</>
);
}

View File

@ -1,11 +0,0 @@
import { SEO } from '@/Components/Atoms/SEO';
import Register from '@/LegacyPages/Public/Register';
export default function Page() {
return (
<>
<SEO title="Wisatin" description="Generated by Wisatin" />
<Register />
</>
);
}

Some files were not shown because too many files have changed in this diff Show More