Compare commits

..
3 Commits
Author SHA1 Message Date
Maulana Sodiqin 9a0fee65a5 fixing so much trouble 2025-05-23 00:28:29 +07:00
Maulana Sodiqin 376e646c10 feat: add vercel json 2025-05-22 22:31:04 +07:00
Maulana Sodiqin f8cd57a1cf chore: update readme and env example 2025-05-20 11:09:14 +07:00
129 changed files with 1157 additions and 3184 deletions
+26 -5
View File
@@ -6,9 +6,10 @@
This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications: This repository is a **monorepo** for all frontend services of IMPHNEN. The monorepo includes three main applications:
1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">gacha website</a>. 1. **Gacha** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Gacha Website</a>.
2. **Backoffice** - Application for internal management. 2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
3. **Dimentorin** - Application for mentoring services. 3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>.
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
## How to install ## How to install
@@ -24,6 +25,15 @@ This repository is a **monorepo** for all frontend services of IMPHNEN. The mono
## How to run ## How to run
### Setup Environment Variables
Before running the applications, you need to set up the environment variables. You can do this by copying the `.env.example` file to `.env` and modifying the values according to your needs.
```sh
cd apps/{appname}
cp .env.example .env
```
### Development ### Development
Use the following commands to run in development mode: Use the following commands to run in development mode:
@@ -40,6 +50,10 @@ Use the following commands to run in development mode:
```sh ```sh
npm run dimentorin:dev npm run dimentorin:dev
``` ```
- **Landing Page**:
```sh
npm run landing:dev
```
### Build ### Build
@@ -57,6 +71,10 @@ Use the following commands to build the applications:
```sh ```sh
npm run dimentorin:build npm run dimentorin:build
``` ```
- **Landing Page**:
```sh
npm run landing:build
```
### Production ### Production
@@ -74,19 +92,22 @@ Use the following commands to run the applications in production mode:
```sh ```sh
npm run dimentorin:prod npm run dimentorin:prod
``` ```
- **Landing Page**:
```sh
npm run landing:prod
```
### Storybook ### Storybook
This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook: This repository uses Storybook to develop, test, and document UI components in an isolated and interactive environment. Below are the commands to work with Storybook:
- **Run Storybook** - **Run Storybook**
This command starts Storybook in development mode, allowing you to view and test UI components interactively. This command starts Storybook in development mode, allowing you to view and test UI components interactively.
```sh ```sh
npm run ui:storybook npm run ui:storybook
``` ```
- **Run Unit Test** - **Run Unit Test**
+1
View File
@@ -0,0 +1 @@
VITE_API_URL=
@@ -18,7 +18,7 @@ import {
RowSelectionState, RowSelectionState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import ModalEditAccount from './_components/modal-edit-account'; import ModalEditAccount from './_components/modal-edit-account';
import { useQueryState } from '../../hook/use-query-state'; import { useQueryState } from '@imphnen-frontend-service/utils';
interface Account { interface Account {
id: number; id: number;
@@ -166,7 +166,7 @@ export const Components: FC = (): ReactElement => {
</Button> </Button>
{showFilter && ( {showFilter && (
<div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg"> <div className="absolute right-0 top-[calc(100%+12px)] z-10 shadow-lg">
<Filter onClose={() => setShowFilter(false)} /> <Filter onClose={() => setShowFilter(false)} options={[]} />
</div> </div>
)} )}
</div> </div>
@@ -10,7 +10,7 @@ import { FC, Fragment, ReactElement, useState } from 'react';
import ModalAddItem from './_components/modal-add-item'; import ModalAddItem from './_components/modal-add-item';
import ModalEditItem from './_components/modal-edit-item'; import ModalEditItem from './_components/modal-edit-item';
import ModalDeleteItem from './_components/modal-delete-item'; import ModalDeleteItem from './_components/modal-delete-item';
import { useQueryState } from '../../hook/use-query-state'; import { useQueryState } from '@imphnen-frontend-service/utils';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false); const [showModalAddItem, setShowModalAddItem] = useState(false);
@@ -2,24 +2,19 @@ import { useForm } from 'react-hook-form';
import { import {
authLoginSchema, authLoginSchema,
TLoginRequest, TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner'; import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => { export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({ const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema), resolver: zodResolver(authLoginSchema),
mode: 'all', mode: 'all',
}); });
const onSubmit = form.handleSubmit((data) => { const { signIn } = useSession();
postLogin.mutate(data, {
onSuccess: () => toast.success("Login sukses"), const onSubmit = form.handleSubmit((data) => signIn(data));
onError: (error) => toast.error(error.message),
});
});
return { return {
form, form,
@@ -32,7 +32,16 @@ export const Components: FC = (): ReactElement => {
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<Button type="submit" size="md" className="w-full"> <Button
disabled={
form.formState.isSubmitting ||
form.formState.isValidating ||
!form.formState.isValid
}
type="submit"
size="md"
className="w-full"
>
Login Login
</Button> </Button>
</form> </form>
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
-18
View File
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
-18
View File
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
@@ -1,18 +0,0 @@
import { FC, ReactElement } from 'react';
import { Outlet } from 'react-router-dom';
import { BackofficeSidebar } from '@imphnen-frontend-service/ui/organisms';
export const AppLayout: FC = (): ReactElement => {
return (
<div className="bg-primary-50 min-h-screen flex justify-center">
<div className="bg-primary-50 min-h-screen w-full flex">
<BackofficeSidebar />
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
<Outlet />
</div>
</div>
</div>
);
};
export default AppLayout;
+100 -3
View File
@@ -1,11 +1,108 @@
import { LoaderFunctionArgs } from 'react-router-dom'; import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/utils';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
//TODO : Fix this later
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
// const fallback = mappingRoutePermissions.find((route) =>
// route.permissions.some((perm) => userPermissions.includes(perm))
// );
// return redirect(fallback?.path ?? '/auth/login');
// };
export const middleware = async ({ request }: LoaderFunctionArgs) => { export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (pathname) return null; if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/dashboard';
}
}
return null; return null;
}; };
+1
View File
@@ -0,0 +1 @@
VITE_API_URL=
+4 -9
View File
@@ -2,24 +2,19 @@ import { useForm } from 'react-hook-form';
import { import {
authLoginSchema, authLoginSchema,
TLoginRequest, TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner'; import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => { export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({ const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema), resolver: zodResolver(authLoginSchema),
mode: 'all', mode: 'all',
}); });
const onSubmit = form.handleSubmit((data) => { const { signIn } = useSession();
postLogin.mutate(data, {
onSuccess: () => toast.success("Login sukses"), const onSubmit = form.handleSubmit((data) => signIn(data));
onError: (error) => toast.error(error.message),
});
});
return { return {
form, form,
+101 -3
View File
@@ -1,11 +1,109 @@
import { LoaderFunctionArgs } from 'react-router-dom'; import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/utils';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/',
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
//TODO : Fix this later
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
// const fallback = mappingRoutePermissions.find((route) =>
// route.permissions.some((perm) => userPermissions.includes(perm))
// );
// return redirect(fallback?.path ?? '/auth/login');
// };
export const middleware = async ({ request }: LoaderFunctionArgs) => { export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (pathname) return null; if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/dashboard';
}
}
return null; return null;
}; };
+1
View File
@@ -0,0 +1 @@
VITE_API_URL=
+4 -7
View File
@@ -2,22 +2,19 @@ import { useForm } from 'react-hook-form';
import { import {
authLoginSchema, authLoginSchema,
TLoginRequest, TLoginRequest,
usePostLogin,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { useSession } from '@imphnen-frontend-service/utils';
export const useLogin = () => { export const useLogin = () => {
const postLogin = usePostLogin();
const form = useForm<TLoginRequest>({ const form = useForm<TLoginRequest>({
resolver: zodResolver(authLoginSchema), resolver: zodResolver(authLoginSchema),
mode: 'all', mode: 'all',
}); });
const onSubmit = form.handleSubmit((data) => { const { signIn } = useSession();
postLogin.mutate(data, {
onSuccess: () => console.log('Success Login'), const onSubmit = form.handleSubmit((data) => signIn(data));
});
});
return { return {
form, form,
+1 -24
View File
@@ -1,14 +1,11 @@
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { import {
authRegisterSchema, authRegisterSchema,
stepOneRegisterSchema,
TRegisterRequest, TRegisterRequest,
usePostRegister, usePostRegister,
} from '@imphnen-frontend-service/service'; } from '@imphnen-frontend-service/service';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useEffect, useState } from 'react';
export const useRegister = () => { export const useRegister = () => {
const postRegister = usePostRegister(); const postRegister = usePostRegister();
@@ -21,31 +18,12 @@ export const useRegister = () => {
password: '', password: '',
confirm_password: '', confirm_password: '',
phone_number: '', phone_number: '',
referral_code: '',
referred_by: '',
student_type: '',
}, },
}); });
const [stepValid, setStepValid] = useState(false);
useEffect(() => {
const subscription = form.watch(() => {
const { fullname, email, password, confirm_password } = form.getValues();
const valid = stepOneRegisterSchema.safeParse({
fullname,
email,
password,
confirm_password,
}).success;
setStepValid(valid);
});
return () => subscription.unsubscribe();
}, [form]);
const onSubmit = form.handleSubmit((data) => { const onSubmit = form.handleSubmit((data) => {
postRegister.mutate(data, { postRegister.mutate(data, {
onSuccess: (data) => toast.success(data.message), onSuccess: () => toast.success('Registrasi sukses'),
onError: (error) => toast.error(error.message), onError: (error) => toast.error(error.message),
}); });
}); });
@@ -53,6 +31,5 @@ export const useRegister = () => {
return { return {
form, form,
onSubmit, onSubmit,
isStepOneValid: stepValid,
}; };
}; };
+101 -3
View File
@@ -1,11 +1,109 @@
import { LoaderFunctionArgs } from 'react-router-dom'; import {
PERMISSIONS,
SessionToken,
SessionUser,
} from '@imphnen-frontend-service/utils';
import { LoaderFunctionArgs, redirect } from 'react-router';
const mappingPublicRoutes = [
'/',
'/auth/login',
'/auth/forgot',
'/auth/new-password',
];
const mappingRoutePermissions = [
{
path: '/dashboard',
permissions: [],
},
{
path: '/users',
permissions: [PERMISSIONS.USERS.READ_LIST],
},
{
path: '/users/create',
permissions: [PERMISSIONS.USERS.CREATE],
},
{
path: '/users/update',
permissions: [PERMISSIONS.USERS.UPDATE],
},
{
path: '/users/detail',
permissions: [PERMISSIONS.USERS.READ_DETAIL],
},
{
path: '/roles',
permissions: [PERMISSIONS.ROLES.READ_LIST],
},
{
path: '/roles/create',
permissions: [PERMISSIONS.ROLES.CREATE],
},
{
path: '/roles/update',
permissions: [PERMISSIONS.ROLES.UPDATE],
},
{
path: '/roles/detail',
permissions: [PERMISSIONS.ROLES.READ_DETAIL],
},
{
path: '/permissions',
permissions: [PERMISSIONS.PERMISSIONS.READ_LIST],
},
{
path: '/permissions/create',
permissions: [PERMISSIONS.PERMISSIONS.CREATE],
},
{
path: '/permissions/update',
permissions: [PERMISSIONS.PERMISSIONS.UPDATE],
},
{
path: '/permissions/detail',
permissions: [PERMISSIONS.PERMISSIONS.READ_DETAIL],
},
];
//TODO : Fix this later
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
// const fallback = mappingRoutePermissions.find((route) =>
// route.permissions.some((perm) => userPermissions.includes(perm))
// );
// return redirect(fallback?.path ?? '/auth/login');
// };
export const middleware = async ({ request }: LoaderFunctionArgs) => { export const middleware = async ({ request }: LoaderFunctionArgs) => {
const url = new URL(request.url); const url = new URL(request.url);
const pathname = url.pathname; const pathname = url.pathname;
const session = SessionUser.get();
const session_token = SessionToken.get();
const token = session_token?.token?.access_token;
const userPermissions =
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
if (pathname) return null; if (mappingPublicRoutes.includes(pathname)) {
if (token) return redirect('/dashboard');
return null;
}
if (!session) return redirect('/auth/login');
const matchedRoute = mappingRoutePermissions.find(
(route) => route.path === pathname
);
if (matchedRoute) {
const hasPermission =
!matchedRoute.permissions ||
matchedRoute.permissions.some((perm) => userPermissions.includes(perm));
if (!hasPermission) {
return '/dashboard';
}
}
return null; return null;
}; };
@@ -1,13 +1,5 @@
# API Endpoint
VITE_API_URL=
# Payload CMS Secret Key (minimum 32 characters)
CMS_SECRET=your-very-long-secret-key CMS_SECRET=your-very-long-secret-key
# PostgreSQL Connection String
CMS_POSTGRES_URL=postgres://user:password@host:port/database CMS_POSTGRES_URL=postgres://user:password@host:port/database
# S3 Storage Configuration
CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com
CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id
CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

@@ -1,5 +0,0 @@
import { ReactNode } from 'react';
export default function Layout({ children }: { children: ReactNode }) {
return <>{children}</>;
}
@@ -1,3 +0,0 @@
export default function Page() {
return <></>;
}
@@ -1,3 +0,0 @@
export default function Page() {
return <></>;
}
@@ -1,71 +0,0 @@
'use client';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import { useRouter } from 'next/navigation';
import { useRef } from 'react';
export function CallToAction() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const router = useRouter();
return (
<section className="w-full py-20 md:py-32 relative overflow-hidden">
{/* Background Elements */}
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background to-primary/20" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,rgba(59,130,246,0.2),transparent_70%)]" />
</div>
<div className="container px-4 md:px-6" ref={ref}>
<motion.div
className="max-w-4xl mx-auto rounded-2xl overflow-hidden border shadow-lg"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
transition={{ duration: 0.5 }}
>
<div className="relative p-8 md:p-12 lg:p-16 bg-background">
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-blue-400/5" />
<div className="relative z-10 text-center">
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-6">
Siap Menjadi{' '}
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Programmer Handal?
</span>
</h2>
<p className="text-lg text-muted-foreground mb-8 max-w-2xl mx-auto">
Bergabunglah dengan komunitas IMPHNEN sekarang dan mulai
perjalanan programming mu dengan cara yang menyenangkan!
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
size="lg"
className="group relative w-full sm:w-auto bg-gradient-to-r from-primary to-blue-600 hover:from-primary/90 hover:to-blue-600/90 transition-all duration-300 font-bold text-white hover:text-white/90 shadow-lg"
onClick={() => router.push('#community')}
>
Gabung Komunitas
</Button>
<Button
size="lg"
variant="outline"
className="border-primary hover:bg-primary/10"
onClick={() => router.push('/events')}
>
Explore Event
</Button>
</div>
</div>
{/* Decorative Elements */}
<div className="absolute -top-12 -left-12 w-24 h-24 rounded-full bg-primary/10 blur-2xl" />
<div className="absolute -bottom-12 -right-12 w-24 h-24 rounded-full bg-blue-400/10 blur-2xl" />
</div>
</motion.div>
</div>
</section>
);
}
@@ -1,247 +0,0 @@
'use client';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
import {
FaDiscord,
FaFacebook,
FaInstagram,
FaLinkedin,
FaTiktok,
} from 'react-icons/fa';
export function Communities() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const communities = [
{
icon: FaFacebook,
title: 'Facebook Group',
description:
'Komunitas aktif dengan 180K+ anggota berdiskusi seputar programming dan sharing meme',
buttonText: 'Gabung Sekarang',
buttonLink: 'https://facebook.com/groups/programmerhandal',
},
{
icon: FaDiscord,
title: 'Discord Server',
description:
'Diskusi real-time dengan developer berbagai level, mulai dari pemula sampai expert!',
buttonText: 'Join Server',
buttonLink: 'https://discord.com/invite/imphnen',
},
{
icon: FaInstagram,
title: 'Instagram',
description:
'Temukan visual tutorial coding & tech trends terkini setiap harinya',
buttonText: 'Follow Kami',
buttonLink: 'https://www.instagram.com/imphnen.dev',
},
{
icon: FaTiktok,
title: 'TikTok',
description:
'Tips coding singkat & trik development praktis dalam 60 detik',
buttonText: 'Follow Sekarang',
buttonLink: 'https://www.tiktok.com/@imphnen',
},
{
icon: FaLinkedin,
title: 'LinkedIn',
description:
'Bangun jaringan profesional dengan perusahaan teknologi ternama & recruiter IT',
buttonText: 'Segera Hadir',
buttonLink: '#',
},
];
const stats = [
{ value: '180K+', label: 'Komunitas Aktif' },
{ value: '25K+', label: 'Postingan/Bulan' },
{ value: '95%', label: 'Respon Cepat' },
{ value: '10K+', label: 'Problem Solved' },
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2,
},
},
};
const cardVariants = {
hidden: { y: 40, opacity: 0, scale: 0.95 },
visible: {
y: 0,
opacity: 1,
scale: 1,
transition: {
type: 'spring',
stiffness: 100,
damping: 20,
},
},
};
const statVariants = {
hidden: { opacity: 0, y: 20 },
visible: (i: number) => ({
opacity: 1,
y: 0,
transition: {
delay: i * 0.1 + 0.5,
duration: 0.6,
},
}),
};
return (
<section
id="community"
className="w-full py-20 md:py-32 relative overflow-hidden container"
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-background" />
<div className="absolute top-0 right-0 w-1/2 h-1/2 bg-gradient-to-bl from-primary/5 to-transparent blur-3xl" />
<div className="absolute bottom-0 left-0 w-1/2 h-1/2 bg-gradient-to-tr from-primary/5 to-transparent blur-3xl" />
</div>
<div className="w-full px-4 md:px-6 lg:px-0" ref={ref}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
>
<h2 className="text-4xl font-bold tracking-tighter md:text-5xl/tight lg:text-6xl text-center mb-6">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/80">
Komunitas Programmer
</span>{' '}
<br className="hidden md:block" />
Terbesar di Indonesia
</h2>
<p className="max-w-4xl mx-auto text-muted-foreground text-center md:text-xl text-balance">
Bergabung dengan jaringan developer profesional untuk berkolaborasi,
belajar, dan berkembang bersama komunitas yang aktif dan suportif
</p>
</motion.div>
{/* First Row - Facebook & Discord */}
<motion.div
className="grid gap-4 grid-cols-1 md:grid-cols-2 w-full mt-16"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{communities.slice(0, 2).map((community, index) => (
<motion.div
key={index}
variants={cardVariants}
className="group relative overflow-hidden rounded-2xl border bg-background p-8 hover:shadow-xl transition-shadow h-full"
whileHover={{
y: -5,
transition: { duration: 0.2 },
}}
>
<div className="absolute top-0 left-0 h-1.5 w-full bg-gradient-to-r from-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">
<div className="mb-6 inline-flex h-14 w-14 items-center justify-center rounded-xl bg-primary/10">
<community.icon className="h-7 w-7 text-primary" />
</div>
<h3 className="mb-4 text-2xl font-bold">{community.title}</h3>
<p className="mb-8 text-muted-foreground text-lg">
{community.description}
</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary/10 group-hover:border-primary/20 group-hover:text-foreground transition-all duration-300 hover:scale-[1.02] border-primary/10 text-base h-12"
onClick={() => window.open(community.buttonLink, '_blank')}
>
{community.buttonText}
</Button>
</div>
<div className="absolute -bottom-40 -right-40 w-80 h-80 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-20 group-hover:-translate-x-20" />
</motion.div>
))}
</motion.div>
{/* Second Row - Instagram, TikTok, LinkedIn */}
<motion.div
className="grid gap-4 grid-cols-1 md:grid-cols-3 w-full mt-8"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{communities.slice(2).map((community, index) => (
<motion.div
key={index + 2}
variants={cardVariants}
className="group relative overflow-hidden rounded-2xl border bg-background p-6 hover:shadow-xl transition-shadow h-full"
whileHover={{
y: -5,
transition: { duration: 0.2 },
}}
>
<div className="absolute top-0 left-0 h-1.5 w-full bg-gradient-to-r from-primary/50 to-primary/30 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<community.icon className="h-6 w-6 text-primary" />
</div>
<h3 className="mb-3 text-xl font-bold">{community.title}</h3>
<p className="mb-6 text-muted-foreground">
{community.description}
</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary/10 group-hover:border-primary/20 group-hover:text-foreground transition-all duration-300 hover:scale-[1.02] border-primary/10 h-11"
onClick={() => window.open(community.buttonLink, '_blank')}
disabled={community.title === 'LinkedIn'}
>
{community.buttonText}
</Button>
</div>
<div className="absolute -bottom-32 -right-32 w-64 h-64 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-10 group-hover:-translate-x-10" />
</motion.div>
))}
</motion.div>
{/* Stats Section */}
<motion.div
className="mt-20 grid grid-cols-2 md:grid-cols-4 gap-8 text-center"
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{stats.map((stat, index) => (
<motion.div
key={index}
variants={statVariants}
custom={index}
className="space-y-3 p-6 rounded-xl bg-background border"
>
<div className="text-5xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/80">
{stat.value}
</div>
<div className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
{stat.label}
</div>
</motion.div>
))}
</motion.div>
</div>
</section>
);
}
@@ -1,164 +0,0 @@
'use client';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import { useRef } from 'react';
export function Events() {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
const events = [
{
image: '/images/event-1.jpg',
title: 'Tech Summit 2024',
description:
'Konferensi teknologi terbesar tahun ini dengan pembicara expert dari perusahaan unicorn',
type: 'offline',
price: 'berbayar',
link: '#',
},
{
image: '/images/event-2.jpg',
title: 'Web Development Bootcamp',
description:
'Pelatihan intensif full-stack development selama 2 minggu secara online',
type: 'online',
price: 'gratis',
link: '#',
},
{
image: '/images/event-3.jpg',
title: 'UI/UX Workshop',
description:
'Workshop praktis membuat prototype aplikasi dengan Figma dan Framer',
type: 'hybrid',
price: 'berbayar',
link: '#',
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2,
},
},
};
const cardVariants = {
hidden: { y: 40, opacity: 0, scale: 0.95 },
visible: {
y: 0,
opacity: 1,
scale: 1,
transition: {
type: 'spring',
stiffness: 100,
damping: 20,
},
},
};
return (
<section
id="events"
className="w-full py-20 md:py-32 relative overflow-hidden container"
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-gradient-to-b from-background via-background to-background" />
<div className="absolute top-0 right-0 w-1/2 h-1/2 bg-gradient-to-bl from-primary/5 to-transparent blur-3xl" />
<div className="absolute bottom-0 left-0 w-1/2 h-1/2 bg-gradient-to-tr from-primary/5 to-transparent blur-3xl" />
</div>
<div className="w-full px-4 md:px-6 lg:px-0" ref={ref}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.8 }}
className="text-center mb-16"
>
<h2 className="text-4xl font-bold tracking-tighter md:text-5xl/tight lg:text-6xl mb-6">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-primary/80">
Event Terbaru
</span>
</h2>
<p className="max-w-2xl mx-auto text-muted-foreground text-center md:text-xl text-balance">
Lihat dan ikuti event terbaru dari IMPHNEN
</p>
</motion.div>
<motion.div
className="grid gap-8 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 w-full"
variants={containerVariants}
initial="hidden"
animate={isInView ? 'visible' : 'hidden'}
>
{events.map((event, index) => (
<motion.div
key={index}
variants={cardVariants}
className="group relative overflow-hidden rounded-2xl border bg-background hover:shadow-xl transition-shadow h-full"
whileHover={{
y: -5,
transition: { duration: 0.2 },
}}
>
<div className="absolute top-0 left-0 h-1.5 w-full bg-gradient-to-r from-primary to-primary/50 opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="relative z-10">
<div className="h-48 bg-muted/20 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-t from-background to-transparent z-10" />
<div className="absolute top-4 right-4 flex gap-2">
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
event.type === 'online'
? 'bg-green-500/10 text-green-500'
: event.type === 'offline'
? 'bg-purple-500/10 text-purple-500'
: 'bg-yellow-500/10 text-yellow-500'
}`}
>
{event.type === 'hybrid'
? 'Hybrid'
: event.type.toUpperCase()}
</span>
<span
className={`px-3 py-1 rounded-full text-sm font-medium ${
event.price === 'gratis'
? 'bg-blue-500/10 text-blue-500'
: 'bg-orange-500/10 text-orange-500'
}`}
>
{event.price.toUpperCase()}
</span>
</div>
</div>
<div className="p-6">
<h3 className="text-2xl font-bold mb-3">{event.title}</h3>
<p className="text-muted-foreground mb-6">
{event.description}
</p>
<Button
variant="outline"
className="w-full group-hover:bg-primary/10 group-hover:border-primary/20 group-hover:text-foreground transition-all duration-300 hover:scale-[1.02] border-primary/10 text-base h-12"
onClick={() => window.open(event.link, '_blank')}
>
Lihat Detail
</Button>
</div>
</div>
<div className="absolute -bottom-40 -right-40 w-80 h-80 bg-gradient-to-tl from-primary/10 to-transparent rounded-full opacity-0 group-hover:opacity-100 transition-all duration-500 group-hover:-translate-y-20 group-hover:-translate-x-20" />
</motion.div>
))}
</motion.div>
</div>
</section>
);
}
@@ -1,21 +0,0 @@
'use client';
import { Button } from '@components/atoms';
import { useRouter } from 'next/navigation';
import { TbCalendarStar } from 'react-icons/tb';
export function ButtonExploreEvent() {
const router = useRouter();
return (
<Button
size="lg"
variant="outline"
className="w-full sm:w-auto group relative overflow-hidden border-primary flex items-center justify-center gap-2"
onClick={() => router.push('/events')}
>
<TbCalendarStar className="size-7" />
<span className="relative">Explore Event</span>
</Button>
);
}
@@ -1,21 +0,0 @@
'use client';
import { Button } from '@components/atoms';
import { useRouter } from 'next/navigation';
import { TbLocationFilled } from 'react-icons/tb';
export function ButtonJoinCommunity() {
const router = useRouter();
return (
<Button
variant="default"
size="lg"
className="group relative w-full sm:w-auto bg-gradient-to-r from-primary to-blue-600 hover:from-primary/90 hover:to-blue-600/90 transition-all duration-300 font-bold text-white hover:text-white/90 shadow-lg"
onClick={() => router.push('#community')}
>
<TbLocationFilled className="size-6" />
Gabung Komunitas
</Button>
);
}
@@ -1,7 +0,0 @@
export function HeroBadge() {
return (
<div className="inline-flex items-center rounded-full border px-3 py-1 text-sm w-fit truncate">
<span>🇮🇩 Komunitas Programmer Indonesia</span>
</div>
);
}
@@ -1,24 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import Image from 'next/image';
export function HeroImage() {
return (
<motion.div
className="relative w-full lg:w-auto mx-auto lg:ml-auto max-w-[600px] mt-8 lg:mt-0"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<Image
src="/logo.png"
alt=""
width={800}
height={600}
className="w-full h-auto object-cover"
priority
/>
</motion.div>
);
}
@@ -1,16 +0,0 @@
export function HeroMainText() {
return (
<div className="space-y-4">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70 leading-tight">
Programmer Handal <br />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Namun Enggan Ngoding
</span>
</h1>
<p className="max-w-[600px] text-muted-foreground md:text-xl lg:text-lg">
Tempat para programmer struggle bersama, berbagi meme, pengalaman,
tutorial, dan tempat yapping yang nyaman
</p>
</div>
);
}
@@ -1,26 +0,0 @@
import { Avatar, AvatarFallback, AvatarImage } from '@components/atoms';
export function HeroStat() {
return (
<div className="flex items-center rounded-full border border-border bg-background p-1 shadow shadow-black/5 md:w-fit">
<div className="flex -space-x-4">
<Avatar>
<AvatarImage src="/maulana.webp" />
<AvatarFallback />
</Avatar>
<Avatar>
<AvatarImage src="/rasyid.webp" />
<AvatarFallback />
</Avatar>
<Avatar>
<AvatarImage src="/ega.webp" />
<AvatarFallback />
</Avatar>
</div>
<p className="px-2 text-xs text-muted-foreground">
<strong className="font-medium text-foreground">180K+</strong>{' '}
Programmer Indonesia telah bergabung
</p>
</div>
);
}
@@ -1,40 +0,0 @@
'use client';
import { ReactNode, useEffect, useState } from 'react';
export function HeroWrapper({ children }: { children: ReactNode }) {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section className="relative w-full py-16 md:py-24 lg:py-32 xl:py-40 overflow-hidden">
<div className="absolute inset-0 -z-10 overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background to-background/50" />
<div
className="absolute top-1/4 -left-20 w-80 h-80 rounded-full bg-gradient-to-r from-primary/20 to-blue-400/20 blur-3xl"
style={{
transform: `translate(${scrollY * 0.1}px, ${scrollY * -0.05}px)`,
opacity: Math.max(0.2, 1 - scrollY * 0.0005),
}}
/>
<div
className="absolute bottom-1/3 -right-20 w-80 h-80 rounded-full bg-gradient-to-r from-blue-400/20 to-primary/20 blur-3xl"
style={{
transform: `translate(${scrollY * -0.1}px, ${scrollY * 0.05}px)`,
opacity: Math.max(0.2, 1 - scrollY * 0.0005),
}}
/>
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
</div>
{children}
</section>
);
}
@@ -1,39 +0,0 @@
'use client';
import { motion } from 'framer-motion';
import { ButtonExploreEvent } from './button-explore-event';
import { ButtonJoinCommunity } from './button-join-community';
import { HeroBadge } from './hero-badge';
import { HeroImage } from './hero-image';
import { HeroMainText } from './hero-main-text';
import { HeroStat } from './hero-stat';
import { HeroWrapper } from './hero-wrapper';
export function Hero() {
return (
<HeroWrapper>
<div className="container px-4 md:px-8 relative">
<div className="grid gap-8 lg:grid-cols-2 lg:gap-16 items-center">
<motion.div
className="flex flex-col justify-center space-y-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<HeroBadge />
<HeroMainText />
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<ButtonJoinCommunity />
<ButtonExploreEvent />
</div>
<HeroStat />
</motion.div>
<HeroImage />
</div>
</div>
</HeroWrapper>
);
}
@@ -1,143 +0,0 @@
'use client';
import { Button } from '@components/atoms';
import { motion, useInView } from 'framer-motion';
import Image from 'next/image';
import { useRef } from 'react';
import { ImPencil2 } from 'react-icons/im';
const testimonials = [
{
author: {
name: 'Ega',
role: 'Backend Engineer',
avatar: '/ega.webp',
},
quote:
'Tralalero Tralala, IMPHENOTRUNASA merupakan anomali yang sering datang ketika melihat orang ngoding',
},
{
author: {
name: 'Rasyid',
role: 'Fullsnack Engineer',
avatar: '/rasyid.webp',
},
quote:
'Dapet job dari meme yang di-share disini. Gak nyangka yapping random bisa jadi koneksi kerja akwokaowkoak',
},
{
author: {
name: 'Maulana Sodiqin',
role: 'Backend Engineer',
avatar: '/maulana.webp',
},
quote: 'Anjay aku bohong? Admin yang bohong',
},
];
export function Testimonials() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, amount: 0.2 });
// Double the testimonials for seamless loop
const duplicatedTestimonials = [...testimonials, ...testimonials];
return (
<section
id="testimoni"
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
ref={ref}
>
<div className="absolute inset-0 -z-10">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,rgba(var(--primary)/0.1),transparent_50%)]" />
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_bottom,rgba(var(--primary)/0.1),transparent_50%)]" />
</div>
<div className="container px-4 md:px-6">
<motion.div
className="flex flex-col items-center justify-center space-y-4 text-center mb-16"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
>
<h2 className="text-3xl font-bold tracking-tighter md:text-4xl lg:text-5xl">
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
Testimoni{' '}
</span>
Member
</h2>
<p className="max-w-[800px] mx-auto text-muted-foreground md:text-lg">
Apa kata mereka yang telah bergabung dengan komunitas IMPHNEN?
</p>
</motion.div>
{/* Infinite Marquee Section */}
<div className="relative w-full overflow-hidden py-4 marquee-container">
<motion.div
className="flex"
animate={{
x: ['0%', '-100%'],
}}
transition={{
duration: 40,
ease: 'linear',
repeat: Infinity,
}}
>
{duplicatedTestimonials.map((testimonial, i) => (
<div
key={i}
className="w-[300px] md:w-[400px] shrink-0 mx-4 p-6 rounded-xl bg-background border border-border/20 hover:border-primary/30 transition-colors group flex flex-col"
style={{ minHeight: '200px' }} // Set a fixed minimum height
>
<div className="flex flex-col gap-4 flex-grow">
<p className="text-muted-foreground italic leading-relaxed line-clamp-4">
&ldquo;{testimonial.quote}&rdquo;
</p>
<div className="mt-auto">
{' '}
{/* This pushes the author info to the bottom */}
<div className="flex items-center gap-4">
<div className="relative h-12 w-12 rounded-full overflow-hidden border-2 border-primary/20 group-hover:border-primary/50 transition-colors">
<Image
src={testimonial.author.avatar}
alt={testimonial.author.name}
width={48}
height={48}
className="object-cover"
unoptimized
/>
</div>
<div>
<h4 className="font-bold">{testimonial.author.name}</h4>
<p className="text-sm text-muted-foreground">
{testimonial.author.role}
</p>
</div>
</div>
</div>
</div>
</div>
))}
</motion.div>
</div>
{/* Call to Action */}
<motion.div
className="mt-16 text-center"
initial={{ opacity: 0, y: 20 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ delay: 0.2 }}
>
<Button
size="lg"
className="group relative w-full sm:w-auto bg-gradient-to-r from-primary to-blue-600 hover:from-primary/90 hover:to-blue-600/90 transition-all duration-300 font-bold text-white hover:text-white/90 shadow-lg"
>
<ImPencil2 className="size-5" />
Tulis dan Tampilkan Disini
</Button>
</motion.div>
</div>
</section>
);
}
@@ -1,17 +0,0 @@
import { CallToAction } from './_components/call-to-action';
import { Communities } from './_components/communites';
import { Events } from './_components/events';
import { Hero } from './_components/hero';
import { Testimonials } from './_components/testimonials';
export default function Page() {
return (
<>
<Hero />
<Events />
<Communities />
<Testimonials />
<CallToAction />
</>
);
}
@@ -1,3 +0,0 @@
export default function Page() {
return <></>;
}
@@ -1,3 +0,0 @@
export default function Page() {
return <></>;
}
@@ -1,13 +0,0 @@
import { ReactNode } from 'react';
import Footer from '../_components/footer';
import { Header } from '../_components/header';
export default function Layout({ children }: { children: ReactNode }) {
return (
<div className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
);
}
@@ -1,3 +0,0 @@
export default function Page() {
return <></>;
}
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { CallToActionSection } from '@/payload-types'; import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { Button } from '@components/atoms'; import { CallToActionSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion'; import { motion, useInView } from 'framer-motion';
import { useRef } from 'react'; import { useRef } from 'react';
@@ -1,8 +1,8 @@
'use client'; 'use client';
import { CommunitiesSection } from '@/payload-types';
import { Button } from '@components/atoms';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { CommunitiesSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion'; import { motion, useInView } from 'framer-motion';
import { useRef } from 'react'; import { useRef } from 'react';
@@ -32,7 +32,7 @@ export function Community(props: CommunitiesSection) {
return ( return (
<section <section
id="community" id="komunitas"
className="w-full py-20 md:py-32 bg-muted relative overflow-hidden" className="w-full py-20 md:py-32 bg-muted relative overflow-hidden"
> >
<div className="absolute inset-0 -z-10"> <div className="absolute inset-0 -z-10">
@@ -1,7 +1,7 @@
'use client'; 'use client';
import { FeaturesSection } from '@/payload-types';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { FeaturesSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion'; import { motion, useInView } from 'framer-motion';
import { useRef } from 'react'; import { useRef } from 'react';
@@ -0,0 +1,159 @@
'use client';
import {
Button,
MenuIcon,
XIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { cn } from '@imphnen-frontend-service/utils';
import Image from 'next/image';
import Link from 'next/link';
import { useEffect, useState } from 'react';
import { SimpleThemeToggle } from './simple-theme-toggle';
export function Header() {
const [isScrolled, setIsScrolled] = useState(false);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<header
className={cn(
'sticky top-0 z-50 w-full transition-all duration-300',
isScrolled
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
: 'bg-transparent'
)}
>
<div className="container flex h-16 items-center justify-between">
<div className="flex items-center gap-2">
<div className="relative overflow-hidden rounded">
<Link href="/">
<Image
src="/logo.png"
alt="IMPHNEN"
width={64}
height={64}
className="object-cover"
/>
</Link>
</div>
</div>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-8">
<Link href="#fitur" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">Fitur</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#komunitas"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Komunitas
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#sumber-belajar"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Sumber Belajar
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link
href="#testimoni"
className="text-sm font-medium relative group"
>
<span className="transition-colors hover:text-primary">
Testimoni
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
</nav>
<div className="flex items-center gap-4">
<SimpleThemeToggle />
<Button
className="hidden md:flex bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300 font-bold text-black hover:text-white cursor-pointer"
onClick={() =>
window.open('https://discord.com/invite/imphnen', '_blank')
}
>
Gabung Discord
</Button>
{/* Mobile Menu Button */}
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
{mobileMenuOpen ? (
<XIcon className="h-6 w-6" />
) : (
<MenuIcon className="h-6 w-6" />
)}
</Button>
</div>
</div>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
<nav className="container flex flex-col py-4 text-center">
<Link
href="#fitur"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Fitur
</Link>
<Link
href="#komunitas"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Komunitas
</Link>
<Link
href="#sumber-belajar"
className="py-3 text-sm font-medium border-b border-border/50"
onClick={() => setMobileMenuOpen(false)}
>
Sumber Belajar
</Link>
<Link
href="#testimoni"
className="py-3 text-sm font-medium"
onClick={() => setMobileMenuOpen(false)}
>
Testimoni
</Link>
<a
href="https://discord.com/invite/imphnen"
target="_blank"
className="mt-4"
>
<Button className="mt-4 bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer">
Gabung Discord
</Button>
</a>
</nav>
</div>
)}
</header>
);
}
@@ -1,11 +0,0 @@
import { buttonVariants } from '@components/atoms';
import { cn } from '@utils/ui';
import Link from 'next/link';
export function ButtonSignin() {
return (
<Link href="/signin" className={cn(buttonVariants(), 'hidden lg:flex')}>
Masuk
</Link>
);
}
@@ -1,14 +0,0 @@
import { buttonVariants } from '@components/atoms';
import { cn } from '@utils/ui';
import Link from 'next/link';
export function ButtonSignup() {
return (
<Link
href="/signup"
className={cn(buttonVariants({ variant: 'outline' }), 'hidden lg:flex')}
>
Daftar
</Link>
);
}
@@ -1,22 +0,0 @@
import Link from 'next/link';
export function DesktopNavigation() {
return (
<nav className="hidden md:flex items-center gap-8">
<Link href="/about" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">About</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/events" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">Event</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
<Link href="/testimonials" className="text-sm font-medium relative group">
<span className="transition-colors hover:text-primary">
Testimonial
</span>
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
</Link>
</nav>
);
}
@@ -1,29 +0,0 @@
'use client';
import { cn } from '@utils/ui';
import { ReactNode, useEffect, useState } from 'react';
export function HeaderWrapper({ children }: { children: ReactNode }) {
const [isScrolled, setIsScrolled] = useState(false);
useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<header
className={cn(
'sticky top-0 z-50 w-full transition-all duration-300',
isScrolled
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
: 'bg-transparent'
)}
>
{children}
</header>
);
}
@@ -1,34 +0,0 @@
'use client';
import { useMobileMenuStore } from '@/stores/mobile-menu-store';
import { ButtonSignin } from './button-signin';
import { ButtonSignup } from './button-signup';
import { DesktopNavigation } from './desktop-navigation';
import { HeaderWrapper } from './header-wrapper';
import { Logo } from './logo';
import { MobileMenuHamburger } from './mobile-menu-hamburger';
import { MobileNavigation } from './mobile-navigation';
import { SimpleThemeToggle } from './simple-theme-toggle';
export function Header() {
const mobileMenuOpen = useMobileMenuStore((s) => s.mobileMenuOpen);
return (
<HeaderWrapper>
<div className="container flex h-16 items-center justify-between">
<Logo />
<DesktopNavigation />
<div className="flex items-center gap-x-2">
<ButtonSignin />
<ButtonSignup />
<SimpleThemeToggle />
<MobileMenuHamburger />
</div>
</div>
{mobileMenuOpen && <MobileNavigation />}
</HeaderWrapper>
);
}
@@ -1,16 +0,0 @@
import Image from 'next/image';
import Link from 'next/link';
export function Logo() {
return (
<Link href="/">
<Image
src="/logo.png"
alt="IMPHNEN"
width={64}
height={64}
className="object-cover"
/>
</Link>
);
}
@@ -1,24 +0,0 @@
'use client';
import { useMobileMenuStore } from '@/stores/mobile-menu-store';
import { Button, MenuIcon, XIcon } from '@components/atoms';
export function MobileMenuHamburger() {
const mobileMenuOpen = useMobileMenuStore((s) => s.mobileMenuOpen);
const toggleMobileMenu = useMobileMenuStore((s) => s.toggleMobileMenu);
return (
<Button
variant="ghost"
size="icon"
className="md:hidden"
onClick={toggleMobileMenu}
>
{mobileMenuOpen ? (
<XIcon className="h-6 w-6" />
) : (
<MenuIcon className="h-6 w-6" />
)}
</Button>
);
}
@@ -1,28 +0,0 @@
import Link from 'next/link';
export function MobileNavigation() {
return (
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
<nav className="container flex flex-col py-4 text-center">
<Link
href="/about"
className="py-3 text-sm font-medium border-b border-border/50"
>
About
</Link>
<Link
href="/events"
className="py-3 text-sm font-medium border-b border-border/50"
>
Event
</Link>
<Link
href="/testimonias"
className="py-3 text-sm font-medium border-b border-border/50"
>
Testimonial
</Link>
</nav>
</div>
);
}
@@ -0,0 +1,154 @@
'use client';
import {
Button,
CodeIcon,
SparklesIcon,
UsersIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { formatCMSImageDataToMedia } from 'apps/landing/src/lib/format';
import { HeroSection } from 'apps/landing/src/payload-types';
import { motion } from 'framer-motion';
import Image from 'next/image';
import { Fragment, useEffect, useState } from 'react';
export function Hero(props: HeroSection) {
const {
badgeText,
buttons,
description,
highlight,
title,
stats,
heroImage,
} = props;
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<section className="relative w-full py-20 md:py-32 lg:py-40 overflow-hidden">
<div className="absolute inset-0 -z-10 overflow-hidden">
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background to-background/50" />
<div
className="absolute top-1/4 -left-20 w-80 h-80 rounded-full bg-gradient-to-r from-primary/20 to-blue-400/20 blur-3xl"
style={{
transform: `translate(${scrollY * 0.1}px, ${scrollY * -0.05}px)`,
opacity: Math.max(0.2, 1 - scrollY * 0.001),
}}
/>
<div
className="absolute bottom-1/3 -right-20 w-80 h-80 rounded-full bg-gradient-to-r from-blue-400/20 to-primary/20 blur-3xl"
style={{
transform: `translate(${scrollY * -0.1}px, ${scrollY * 0.05}px)`,
opacity: Math.max(0.2, 1 - scrollY * 0.001),
}}
/>
<div className="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-[size:40px_40px]" />
</div>
<div className="container px-4 md:px-6 relative">
<div className="grid gap-6 lg:grid-cols-2 lg:gap-12 items-center">
<motion.div
className="flex flex-col justify-center space-y-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="inline-flex items-center rounded-full border px-3 py-1 text-sm w-fit">
<SparklesIcon className="mr-1 h-3.5 w-3.5 text-primary" />
<span>{badgeText}</span>
</div>
<div className="space-y-4">
<h1 className="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
{title} <br />
<span className="bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
{highlight}
</span>
</h1>
<p className="max-w-[600px] text-muted-foreground md:text-xl">
{description}
</p>
</div>
<div className="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
<Button
size="lg"
className="w-full sm:w-auto bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300 font-bold text-black hover:text-white cursor-pointer"
onClick={() => window.open(buttons.primaryUrl, '_blank')}
>
{buttons.primaryLabel}
</Button>
<Button
size="lg"
variant="outline"
className="w-full sm:w-auto group relative overflow-hidden border-primary"
onClick={() => window.open(buttons.secondaryUrl, '_blank')}
>
<span className="absolute inset-0 bg-gradient-to-r from-primary/10 to-blue-400/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300" />
<span className="relative">{buttons.secondaryLabel}</span>
</Button>
</div>
<div className="flex flex-wrap justify-center gap-6 sm:gap-8">
{stats?.map(({ value, label }, i) => (
<Fragment key={i}>
<div className="flex flex-col items-center">
<div className="text-2xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-primary to-blue-400">
{value}
</div>
<div className="text-xs text-muted-foreground">{label}</div>
</div>
{i < stats.length - 1 && (
<div className="hidden sm:block h-10 border-r border-border mx-4" />
)}
</Fragment>
))}
</div>
</motion.div>
<motion.div
className="relative w-full lg:w-auto mx-auto lg:ml-auto"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="relative">
<div className="absolute -top-6 -left-6 w-12 h-12 rounded-lg border border-primary/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
<CodeIcon className="h-6 w-6 text-primary" />
</div>
<div className="absolute -bottom-6 -right-6 w-12 h-12 rounded-lg border border-blue-400/30 bg-background/50 backdrop-blur-sm flex items-center justify-center">
<UsersIcon className="h-6 w-6 text-blue-400" />
</div>
<div className="relative z-10 rounded-2xl overflow-hidden border shadow-2xl">
<div className="absolute inset-0 bg-gradient-to-tr from-primary/10 to-blue-400/10" />
{(() => {
const media = formatCMSImageDataToMedia(heroImage);
if (!media) return null;
return (
<Image
src={media.url!}
alt={media.alt}
width={600}
height={500}
className="w-full h-auto object-cover"
/>
);
})()}
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
}
@@ -1,8 +1,8 @@
'use client'; 'use client';
import { LearningResourcesSection } from '@/payload-types';
import { Button } from '@components/atoms';
import { Icon } from '@iconify/react'; import { Icon } from '@iconify/react';
import { Button } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { LearningResourcesSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion'; import { motion, useInView } from 'framer-motion';
import { useRef } from 'react'; import { useRef } from 'react';
@@ -1,6 +1,10 @@
'use client'; 'use client';
import { Button, MoonIcon, SunIcon } from '@components/atoms'; import {
Button,
MoonIcon,
SunIcon,
} from '@imphnen-frontend-service/shadcn-ui/atoms';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@@ -1,8 +1,8 @@
'use client'; 'use client';
import { formatCMSImageDataToMedia } from '@/lib/format'; import { QuoteIcon } from '@imphnen-frontend-service/shadcn-ui/atoms';
import { TestimonialsSection } from '@/payload-types'; import { formatCMSImageDataToMedia } from 'apps/landing/src/lib/format';
import { QuoteIcon } from '@components/atoms'; import { TestimonialsSection } from 'apps/landing/src/payload-types';
import { motion, useInView } from 'framer-motion'; import { motion, useInView } from 'framer-motion';
import Image from 'next/image'; import Image from 'next/image';
import { useRef } from 'react'; import { useRef } from 'react';
+14 -7
View File
@@ -1,12 +1,15 @@
import { poppins } from '@/lib/fonts';
import '@/styles/globals.css';
import { type Metadata } from 'next'; import { type Metadata } from 'next';
import { Inter } from 'next/font/google';
import '../../styles/globals.css';
import Footer from './_components/footer';
import { Header } from './_components/header';
import { ThemeProvider } from './_components/theme-provider'; import { ThemeProvider } from './_components/theme-provider';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'IMPHNEN - Ingin Menjadi Programmer Handal Namung Enggan Ngonding', title: 'IMPHNEN - Ingin Menjadi Programmer Handal?',
description: description: 'Komunitas belajar programming untuk semua level',
'Komunitas Ingin Menjadi Programmer Handal Namung Enggan Ngonding',
}; };
export default function RootLayout({ export default function RootLayout({
@@ -16,14 +19,18 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="id" suppressHydrationWarning> <html lang="id" suppressHydrationWarning>
<body className={poppins.className}> <body className={inter.className}>
<ThemeProvider <ThemeProvider
attribute="class" attribute="class"
defaultTheme="system" defaultTheme="system"
enableSystem enableSystem
disableTransitionOnChange disableTransitionOnChange
> >
{children} <div className="flex min-h-screen flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>
+43
View File
@@ -0,0 +1,43 @@
import { getGlobalsCallToActionSection } from '../../services/get-globals-call-to-action-section';
import { getGlobalsCommunitiesSection } from '../../services/get-globals-communitues-section';
import { getGlobalsFeaturesSection } from '../../services/get-globals-features-section';
import { getGlobalsHeroSection } from '../../services/get-globals-hero-section';
import { getGlobalsLearningResourcesSection } from '../../services/get-globals-learning-resources-section';
import { getGlobalsTestimonialsSection } from '../../services/get-globals-testimonials-section';
import { CallToAction } from './_components/call-to-action';
import { Community } from './_components/community';
import { Features } from './_components/features';
import { Hero } from './_components/hero';
import { LearningResources } from './_components/learning-resources';
import { Testimonials } from './_components/testimonials';
export const revalidate = 10; // Seconds
export default async function Page() {
const [
heroData,
featuresData,
communitiesData,
learningResourcesData,
testimonialsData,
callToActionData,
] = await Promise.all([
getGlobalsHeroSection(),
getGlobalsFeaturesSection(),
getGlobalsCommunitiesSection(),
getGlobalsLearningResourcesSection(),
getGlobalsTestimonialsSection(),
getGlobalsCallToActionSection(),
]);
return (
<>
<Hero {...heroData} />
<Features {...featuresData} />
<Community {...communitiesData} />
<LearningResources {...learningResourcesData} />
<Testimonials {...testimonialsData} />
<CallToAction {...callToActionData} />
</>
);
}
@@ -2,8 +2,8 @@
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import config from '@/payload.config';
import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views'; import { NotFoundPage, generatePageMetadata } from '@payloadcms/next/views';
import config from '../../../../payload.config';
import { importMap } from '../importMap'; import { importMap } from '../importMap';
type Args = { type Args = {
@@ -2,8 +2,8 @@
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import config from '@/payload.config';
import { RootPage, generatePageMetadata } from '@payloadcms/next/views'; import { RootPage, generatePageMetadata } from '@payloadcms/next/views';
import config from '../../../../payload.config';
import { importMap } from '../importMap'; import { importMap } from '../importMap';
type Args = { type Args = {
@@ -1,6 +1,5 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@/payload.config';
import '@payloadcms/next/css'; import '@payloadcms/next/css';
import { import {
REST_DELETE, REST_DELETE,
@@ -10,6 +9,7 @@ import {
REST_POST, REST_POST,
REST_PUT, REST_PUT,
} from '@payloadcms/next/routes'; } from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const GET = REST_GET(config); export const GET = REST_GET(config);
export const POST = REST_POST(config); export const POST = REST_POST(config);
@@ -1,7 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@/payload.config';
import '@payloadcms/next/css'; import '@payloadcms/next/css';
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'; import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const GET = GRAPHQL_PLAYGROUND_GET(config); export const GET = GRAPHQL_PLAYGROUND_GET(config);
@@ -1,7 +1,7 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@/payload.config';
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'; import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes';
import config from '../../../../payload.config';
export const POST = GRAPHQL_POST(config); export const POST = GRAPHQL_POST(config);
+1 -1
View File
@@ -1,10 +1,10 @@
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */ /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */ /* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
import config from '@/payload.config';
import '@payloadcms/next/css'; import '@payloadcms/next/css';
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'; import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts';
import type { ServerFunctionClient } from 'payload'; import type { ServerFunctionClient } from 'payload';
import React from 'react'; import React from 'react';
import config from '../../payload.config';
import { importMap } from './admin/importMap.js'; import { importMap } from './admin/importMap.js';
import './custom.scss'; import './custom.scss';
-8
View File
@@ -1,8 +0,0 @@
// fonts.ts
import { Poppins } from 'next/font/google';
export const poppins = Poppins({
subsets: ['latin'],
weight: ['300', '400', '500', '600', '700'],
display: 'swap',
});
-6
View File
@@ -1,6 +0,0 @@
import type { paths } from '@/openapi-types';
import createClient from 'openapi-fetch';
export const fetcher = createClient<paths>({
baseUrl: 'https://api.imphnen.dev',
});
File diff suppressed because it is too large Load Diff

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