Compare commits

..
Author SHA1 Message Date
arraysid 5aa66540ba feat: add feature request navigation 2025-05-23 01:20:29 +07:00
arraysid 01da8339f4 feat: implement signup and verification basic functionality with forms and actions 2025-05-23 01:15:12 +07:00
arraysid e13e5b4de2 feat: implement events and testimonials base pages 2025-05-23 01:14:49 +07:00
arraysid 08b4c39c03 feat: update form label and message styles & add select component 2025-05-23 01:14:10 +07:00
arraysid 610a1b7556 feat: create fetchPostSignin function for handling signin API requests 2025-05-20 10:02:18 +07:00
arraysid f5f715175c feat: restructure signin functionality with new hooks and improved validation 2025-05-20 10:00:52 +07:00
arraysid 1ddb2f72c6 feat: refactor signin action and form to use validation schema 2025-05-20 04:10:29 +07:00
arraysid b04f687199 feat: add signin page with functionality 2025-05-19 19:01:55 +07:00
arraysid 50cc24e46f feat: implement cookie management for access and refresh tokens 2025-05-19 15:02:02 +07:00
arraysid 4c7bb18a4f feat: replace ThemeProvider with Providers component 2025-05-19 14:10:11 +07:00
arraysid 0bc7d084d5 feat: change all public page to public dir 2025-05-19 13:39:03 +07:00
arraysid 795bcb6ab1 feat: add open api client and types for API integration 2025-05-19 13:33:09 +07:00
arraysid 2de5fd56d8 feat: add signin and signup page 2025-05-19 13:32:40 +07:00
arraysid d078999eec feat: add basic events section 2025-05-19 12:45:52 +07:00
arraysid a8dd867e1a feat: update button text in testimonials component for clarity 2025-05-17 15:37:40 +07:00
arraysid ff65424ff9 feat: add CallToAction component to encourage community engagement 2025-05-17 15:31:00 +07:00
arraysid 2fc625e454 feat: add testimonials component to showcase user feedback 2025-05-17 15:18:48 +07:00
arraysid d1bc9e10d7 feat: enhance communities component with improved layout and updated content 2025-05-17 11:41:39 +07:00
arraysid 017b5309a5 feat: add communites basic component to enhance community engagement section 2025-05-17 11:14:51 +07:00
arraysid e5844327a5 feat: remove unused images 2025-05-17 10:41:22 +07:00
arraysid 7874800d5e feat: add herostat component 2025-05-17 10:35:40 +07:00
arraysid 32d482ba4c chore: move unused components to deprecated dir 2025-05-17 09:17:33 +07:00
arraysid 502e45b4ff feat: reforming hero section 2025-05-17 08:46:07 +07:00
arraysid 7411fd2a13 chore: update import and tsconfig 2025-05-17 05:42:47 +07:00
arraysid 9df5bf8bbf fix: refactor homepage into dedicated home directory 2025-05-17 05:19:18 +07:00
arraysid 9548272340 feat: implement poppins font 2025-05-17 05:12:27 +07:00
arraysid d1abc3c7d4 feat: make header components atomic & remove unused menu 2025-05-17 03:52:41 +07:00
268 changed files with 11265 additions and 5953 deletions

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
# API Endpoint
VITE_API_URL=
# Payload CMS Secret Key (minimum 32 characters)
CMS_SECRET=your-very-long-secret-key
# PostgreSQL Connection String
CMS_POSTGRES_URL=postgres://user:password@host:port/database
# S3 Storage Configuration
CMS_STORAGE_ENDPOINT=https://your-s3-endpoint.com
CMS_STORAGE_ACCESS_KEY_ID=your-access-key-id
CMS_STORAGE_SECRET_ACCESS_KEY=your-secret-access-key
CMS_STORAGE_REGION=ap-southeast-1
-33
View File
@@ -1,33 +0,0 @@
name: Deploy Landing
on:
push:
paths:
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
port: ${{ secrets.SSH_PORT }}
script: |
set -e
if [ ! -d ~/imphnen-frontend-service ]; then
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service
else
cd ~/imphnen-frontend-service && git pull
fi
echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" > ~/imphnen-frontend-service/apps/landing/.env
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml down || true
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d
docker image prune -af
+6
View File
@@ -62,3 +62,9 @@ storybook-static
# Next.js # Next.js
.next .next
out out
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md
+5 -26
View File
@@ -6,10 +6,9 @@
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 <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>. 2. **Backoffice** - Application for internal management.
3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>. 3. **Dimentorin** - Application for mentoring services.
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
## How to install ## How to install
@@ -25,15 +24,6 @@ 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:
@@ -50,10 +40,6 @@ 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
@@ -71,10 +57,6 @@ 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
@@ -92,22 +74,19 @@ 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
@@ -1 +0,0 @@
VITE_API_URL=
@@ -2,19 +2,24 @@ 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'; import { toast } from 'sonner';
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 { signIn } = useSession(); const onSubmit = form.handleSubmit((data) => {
postLogin.mutate(data, {
const onSubmit = form.handleSubmit((data) => signIn(data)); onSuccess: () => toast.success("Login sukses"),
onError: (error) => toast.error(error.message),
});
});
return { return {
form, form,
@@ -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 '@imphnen-frontend-service/utils'; import { useQueryState } from '../../hook/use-query-state';
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)} options={[]} /> <Filter onClose={() => setShowFilter(false)} />
</div> </div>
)} )}
</div> </div>
@@ -0,0 +1,18 @@
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;
@@ -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 '@imphnen-frontend-service/utils'; import { useQueryState } from '../../hook/use-query-state';
export const Components: FC = (): ReactElement => { export const Components: FC = (): ReactElement => {
const [showModalAddItem, setShowModalAddItem] = useState(false); const [showModalAddItem, setShowModalAddItem] = useState(false);
@@ -0,0 +1,18 @@
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;
@@ -32,16 +32,7 @@ export const Components: FC = (): ReactElement => {
size="lg" size="lg"
className="w-full" className="w-full"
/> />
<Button <Button type="submit" size="md" className="w-full">
disabled={
form.formState.isSubmitting ||
form.formState.isValidating ||
!form.formState.isValid
}
type="submit"
size="md"
className="w-full"
>
Login Login
</Button> </Button>
</form> </form>
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,18 @@
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
@@ -0,0 +1,18 @@
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;
@@ -0,0 +1,18 @@
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;
+3 -100
View File
@@ -1,108 +1,11 @@
import { import { LoaderFunctionArgs } from 'react-router-dom';
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 (mappingPublicRoutes.includes(pathname)) { if (pathname) return null;
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
@@ -1 +0,0 @@
VITE_API_URL=
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg width="317" height="170" viewBox="0 0 317 170" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="317" height="169.613" fill="url(#pattern0_164_317)"/>
<defs>
<pattern id="pattern0_164_317" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_164_317" transform="scale(0.00184502 0.00344828)"/>
</pattern>
<image id="image0_164_317" width="542" height="290" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAh4AAAEiCAYAAAC2k9sBAAAABGdBTUEAALGPC/xhBQAACklpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAAEiJnVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKLine truncated
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 351 KiB

-9
View File
@@ -1,9 +0,0 @@
<svg width="138" height="52" viewBox="0 0 138 52" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect width="137.505" height="51.7538" fill="url(#pattern0_164_316)"/>
<defs>
<pattern id="pattern0_164_316" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_164_316" transform="matrix(0.000440723 0 0 0.00117096 -0.173204 -1.3185)"/>
</pattern>
<image id="image0_164_316" width="3000" height="3000" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAC7gAAAu4CAYAAABxFfV5AAAACXBIWXMAAC4jAAAuIwF4pT92AAX6AElEQVR4nOzaMQEAIAzAsIF/z0MGPRIFFdCzuwMAAAAAAAAAAAAAAL/d3wEAAAAAAAAAAAAAADBjcAcAAAAAAAAAAAAAIMLgDgAAAAAAAAAAAABAgsEdAAAAAAAAAAAAAIAEgzsAAAAAAAAAAAAAAAkGdwAAAAAAAAAAAAAAEgzuAAAAAAAAAAAAAAAkGNwBAAAAAAAAAAAAAEgwuAMAAAAAAAAAAAAAkGBwBwAAAAAAAAAAAAAgweAOAAAAAAAAAAAAAECCwR0AAAAAAAAAAAAAgASDOwAAAAAAAAAAAAAACQZ3AAAALine truncated
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 511 KiB

@@ -1,73 +0,0 @@
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { motion } from 'framer-motion'
export function HeroSection() {
return (
<section className="relative w-full py-20 md:py-28 lg:py-32">
{/* Background gradients and shapes */}
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
className="absolute top-10 -left-64 size-[448px] bg-primary-200 rounded-full justify-center items-center hidden lg:flex"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, delay: 0.1 }}
viewport={{ once: true }}
className="size-[296px] bg-primary-50 rounded-full"
/>
</motion.div>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
className="absolute top-3/5 -right-56 size-96 bg-primary-500 rounded-full flex justify-center items-center lg:top-1/2 lg:-right-64 lg:size-[600px]"
>
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ duration: 0.3, delay: 0.1 }}
viewport={{ once: true }}
className="size-64 bg-primary-50 rounded-full lg:size-[400px]"
/>
</motion.div>
<div className="absolute -top-28 -left-20 size-[248px] bg-radial from-primary-300 rounded-full blur-3xl" />
<div className="absolute top-0 -right-72 size-[480px] bg-radial from-primary-400 rounded-full blur-[80px]" />
</div>
<div className="flex flex-col items-center gap-y-10">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
viewport={{ once: true }}
className="flex flex-col items-center gap-y-6"
>
<h1 className="text-2xl font-semibold text-primary-500 md:text-4xl md:font-bold lg:text-5xl">DIMENTORIN</h1>
<p className="max-w-52 text-center font-semibold text-xl leading-tight text-primary-500 md:max-w-72 lg:text-4xl lg:max-w-lg">
Learn IT smarter with AI & expert mentors!
</p>
<p className="max-w-3xs text-xs text-neutral-500 text-center font-medium md:max-w-lg md:text-base lg:text-lg lg:max-w-[646px]">
Apakah kamu siap untuk grind skill TI-mu ke level Ultimate? Dengan AI sebagai navigator dan mentor profesional sebagai sensei-mu, perjalanan belajarmu akan lebih cepat, efektif, dan penuh EXP!
</p>
</motion.div>
<motion.div initial={{ opacity: 0, y: 30 }} whileInView={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.2 }} viewport={{ once: true }}>
<Button
type="button"
size="sm"
className="text-[10px] h-auto px-2.5 py-2 md:text-sm"
>
Belajar Dengan AI Sekarang
</Button>
</motion.div>
</div>
</section>
)
}
@@ -1,53 +0,0 @@
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { FC } from "react";
import { motion } from 'framer-motion'
import { For } from "../../../_components/logic/for";
const OFFERS: { title: string; description: string; image: string }[] = [
{ title: "AI-Powered Learning", description: "Level Up Instantly!", image: "/image/what-we-offer/ai-powered-learning.webp" },
{ title: "1-on-1 Mentoring", description: "Sensei dari Dunia Nyata!", image: "/image/what-we-offer/1-on-1-mentoring.webp" },
{ title: "Community & Resources", description: "Connect dengan Fellow Devs!", image: "/image/what-we-offer/community.webp" },
]
export const WhatWeOfferSection: FC = () => {
return (
<section className="w-full mx-auto px-8 pb-4 md:px-[60px] lg:px-20 lg:pt-4">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="mb-6 flex justify-center lg:mb-10"
>
<Button
type="button"
size="sm"
variant="bordered"
className="h-auto px-3 py-2 font-semibold bg-primary-100 md:text-base md:px-5 md:py-2.5 lg:text-2xl lg:px-6 lg:py-3"
>
Apa yang Kami Tawarkan
</Button>
</motion.div>
<div className="grid gap-6 max-w-7xl mx-auto md:grid-cols-3">
<For data={OFFERS}>
{(offer) => (
<motion.div
className="relative aspect-[3/2] overflow-hidden rounded-2xl md:aspect-[3/4] lg:aspect-[5/6]"
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<img src={offer.image} alt={offer.title} className="size-full object-cover" />
<div
className="absolute bottom-0 inset-x-0 p-4 h-1/2 text-primary-50 bg-gradient-to-t from-primary-500 via-primary-500/80 flex flex-col items-center justify-end gap-y-1 md:items-start lg:px-6 lg:pb-8"
>
<p className="font-semibold lg:text-[26px] lg:font-bold">{offer.title}</p>
<p className="text-xs lg:text-[22px] lg:font-medium">{offer.description}</p>
</div>
</motion.div>
)}
</For>
</div>
</section>
)
}
Loaded 50 of 268 files, more files were not shown because too many files have changed in this diff. Show more