Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b266aa8796 | ||
|
|
57222b655c | ||
|
|
72888bd090 | ||
|
|
a5bde58bc8 | ||
|
|
302ee37ca1 |
@@ -0,0 +1,33 @@
|
|||||||
|
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
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 351 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 511 KiB |
@@ -0,0 +1,73 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { HeroSection } from './_components/hero-section';
|
||||||
|
import { WhatWeOfferSection } from './_components/what-we-offer-section';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HeroSection />
|
||||||
|
<WhatWeOfferSection />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { CloseOutlined, MenuOutlined } from "@ant-design/icons";
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import { Link, useLocation } from "react-router-dom";
|
||||||
|
import { For } from "../../_components/logic/for";
|
||||||
|
import { Show } from "../../_components/logic/show";
|
||||||
|
|
||||||
|
const MENUS: { label: string; href: string }[] = [
|
||||||
|
{ label: "Home", href: "/" },
|
||||||
|
{ label: 'Mentoring', href: '/mentoring' },
|
||||||
|
{ label: 'Resources', href: '/resources' },
|
||||||
|
{ label: 'Articles', href: '/articles' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const Header: FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const [expandMenu, setExpandMenu] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full px-8 pt-8 top-0 z-50 md:px-[60px] md:pt-[60px] lg:px-20 sticky">
|
||||||
|
<header
|
||||||
|
className="bg-white shadow-lg rounded-lg h-12 flex justify-between w-full max-w-7xl xl:mx-auto md:h-[60px] lg:h-[71px]"
|
||||||
|
aria-roledescription="nav"
|
||||||
|
>
|
||||||
|
<div className="flex w-full items-center justify-between p-4 md:px-8 md:py-2.5">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img
|
||||||
|
src="/logos/simple.svg"
|
||||||
|
alt="IMPHNEN Logo"
|
||||||
|
className="h-8 md:h-10 lg:h-[52px] w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col items-center py-2 fixed top-24 bg-white shadow-lg rounded-lg transition-all duration-300",
|
||||||
|
"md:top-32 lg:static lg:py-0 lg:flex-row lg:bg-transparent lg:shadow-none lg:gap-x-4",
|
||||||
|
!expandMenu ? "-right-96" : "right-8 md:right-[60px]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<For data={MENUS}>
|
||||||
|
{(menu) => (
|
||||||
|
<Button
|
||||||
|
key={menu.label}
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
className={cn(
|
||||||
|
"px-10 py-1.5 text-neutral-300 hover:text-neutral-400 lg:px-2.5 lg:py-2",
|
||||||
|
location.pathname === menu.href && "text-primary-500 hover:text-primary-500"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link to={menu.href}>{menu.label}</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<Button type="button" className="px-12 py-1 md:hidden">Login</Button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
className={cn("transition-transform duration-300 rotate-0 lg:hidden", expandMenu && "rotate-90")}
|
||||||
|
onClick={() => setExpandMenu(prev => !prev)}
|
||||||
|
>
|
||||||
|
<Show condition={!expandMenu} fallback={<CloseOutlined />}>
|
||||||
|
<MenuOutlined />
|
||||||
|
</Show>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Outlet } from "react-router-dom";
|
||||||
|
import { Header } from "./_components/header";
|
||||||
|
|
||||||
|
export default function Layout() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-screen flex-col bg-primary-50">
|
||||||
|
<Header />
|
||||||
|
<main className="flex-1 z-0">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export interface ForProps<T, U> {
|
||||||
|
data: readonly T[]
|
||||||
|
children: (item: T, index: number) => U | null
|
||||||
|
fallback?: React.ReactNode | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A functional component that renders a list of children components from a given
|
||||||
|
* array of data.
|
||||||
|
*
|
||||||
|
* @param data - The array of data to render
|
||||||
|
* @param children - A function that takes the current item and index and returns
|
||||||
|
* the child component to render
|
||||||
|
* @param fallback - An optional fallback component to render when the data is empty
|
||||||
|
*
|
||||||
|
* @returns An array of rendered child components if the data is not empty, otherwise
|
||||||
|
* the fallback component if it is provided, null otherwise
|
||||||
|
*/
|
||||||
|
export function For<T, U extends React.JSX.Element>({
|
||||||
|
data,
|
||||||
|
children,
|
||||||
|
fallback = null
|
||||||
|
}: ForProps<T, U>): (U | null)[] | React.ReactNode | null {
|
||||||
|
if (!Array.isArray(data) || !data?.length) return fallback
|
||||||
|
|
||||||
|
return data.map((item, idx) => children(item, idx))
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
export interface ShowProps {
|
||||||
|
condition: boolean
|
||||||
|
children: React.ReactNode
|
||||||
|
fallback?: React.ReactNode | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conditionally renders the `children` component if `condition` is true. If
|
||||||
|
* `condition` is false, renders the `fallback` component instead.
|
||||||
|
*
|
||||||
|
* If `fallback` is `null`, renders nothing.
|
||||||
|
*
|
||||||
|
* @param condition - The condition to check
|
||||||
|
* @param children - The component to render if `condition` is true
|
||||||
|
* @param fallback - The component to render if `condition` is false
|
||||||
|
*/
|
||||||
|
export function Show({ condition, children, fallback = null }: ShowProps) {
|
||||||
|
return condition ? children : fallback
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
|
||||||
return <>Hallo</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Components;
|
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
const { composePlugins, withNx } = require('@nx/next');
|
const { composePlugins, withNx } = require('@nx/next');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
|
/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
nx: { svgr: false },
|
nx: { svgr: false },
|
||||||
|
output: 'standalone',
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = composePlugins(withNx)(nextConfig);
|
module.exports = composePlugins(withNx)(nextConfig);
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
|
import { baiJamjureeFont } from '@/lib/fonts';
|
||||||
import { Card } from '@components';
|
import { Card } from '@components';
|
||||||
|
import { cn } from '@utils';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { AnimatedBackground } from './_components/animated-background';
|
import { AnimatedBackground } from './_components/animated-background';
|
||||||
|
|
||||||
export default function Layout({ children }: { children: ReactNode }) {
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background/20 relative flex min-h-[100dvh] items-center justify-center p-4">
|
<div
|
||||||
|
className={cn(
|
||||||
|
baiJamjureeFont.className,
|
||||||
|
'bg-background/20 relative flex min-h-[100dvh] items-center justify-center p-4'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<AnimatedBackground />
|
<AnimatedBackground />
|
||||||
<div className="z-10 w-full">
|
<div className="z-10 w-full">
|
||||||
<Card className="bg-background/90 mx-auto w-full max-w-[360px] p-6 shadow-xl backdrop-blur-lg sm:max-w-md sm:p-8">
|
<Card className="bg-background/90 mx-auto w-full max-w-[360px] p-6 shadow-xl backdrop-blur-lg sm:max-w-md sm:p-8">
|
||||||
|
|||||||
@@ -32,21 +32,21 @@ export default function ProjectsVote() {
|
|||||||
title: 'IMPHNEN Certificate',
|
title: 'IMPHNEN Certificate',
|
||||||
description: 'Cetak sertifikat keren secara instan untuk anggota IMPHNEN',
|
description: 'Cetak sertifikat keren secara instan untuk anggota IMPHNEN',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'IMPHNEN Roadmap by Vote',
|
|
||||||
description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const completedItems = [
|
const completedItems = [
|
||||||
{
|
{
|
||||||
title: 'IMPHNEN Events Page',
|
title: 'IMPHNEN List Event',
|
||||||
description:
|
description:
|
||||||
'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti',
|
'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'IMPHNEN Testimoni Page',
|
title: 'IMPHNEN Testimoni',
|
||||||
description: 'Berikan testimonial buat komunitas IMPHNEN',
|
description: 'Berikan testimonial gokil buat komunitas IMPHNEN',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'IMPHNEN Roadmap by Vote',
|
||||||
|
description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,343 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarImage,
|
|
||||||
Button,
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
Drawer,
|
|
||||||
DrawerClose,
|
|
||||||
DrawerContent,
|
|
||||||
DrawerFooter,
|
|
||||||
DrawerHeader,
|
|
||||||
DrawerTitle,
|
|
||||||
DrawerTrigger,
|
|
||||||
Input,
|
|
||||||
Label,
|
|
||||||
Textarea,
|
|
||||||
} from '@components';
|
|
||||||
import { cn } from '@utils';
|
|
||||||
import * as React from 'react';
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
import { LuArrowLeft, LuUpload, LuUser, LuX } from 'react-icons/lu';
|
|
||||||
|
|
||||||
export function TestimonialPopup() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const isDesktop = useMediaQuery('(min-width: 768px)');
|
|
||||||
|
|
||||||
if (isDesktop) {
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button>Tambah Testimoni</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
|
||||||
<DialogHeader className="pb-2">
|
|
||||||
<DialogTitle>Tulis Testimoni</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<TestimonialForm setOpen={setOpen} />
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
|
||||||
<DrawerTrigger asChild>
|
|
||||||
<Button>Tambah Testimoni</Button>
|
|
||||||
</DrawerTrigger>
|
|
||||||
<DrawerContent className="max-h-[85vh]">
|
|
||||||
<DrawerHeader className="pb-0">
|
|
||||||
<DrawerTitle>Tulis Testimoni</DrawerTitle>
|
|
||||||
</DrawerHeader>
|
|
||||||
<div className="overflow-y-auto px-4">
|
|
||||||
<TestimonialForm setOpen={setOpen} className="py-2" />
|
|
||||||
</div>
|
|
||||||
<DrawerFooter className="pt-2 pb-4">
|
|
||||||
<DrawerClose asChild>
|
|
||||||
<Button variant="bordered">Batal</Button>
|
|
||||||
</DrawerClose>
|
|
||||||
</DrawerFooter>
|
|
||||||
</DrawerContent>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TestimonialFormProps extends React.ComponentProps<'div'> {
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function TestimonialForm({ className, setOpen }: TestimonialFormProps) {
|
|
||||||
const [step, setStep] = useState(1);
|
|
||||||
const [testimonialError, setTestimonialError] = useState(false);
|
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const [avatarPreview, setAvatarPreview] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
role: '',
|
|
||||||
testimonial: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string) => {
|
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) {
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
alert('Hanya file gambar yang diizinkan');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (event) => {
|
|
||||||
if (event.target?.result) {
|
|
||||||
setAvatarPreview(event.target.result as string);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeAvatar = (e: React.MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setAvatarPreview(null);
|
|
||||||
if (fileInputRef.current) {
|
|
||||||
fileInputRef.current.value = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
|
||||||
fileInputRef.current?.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateStep1 = () => {
|
|
||||||
return formData.name.trim() !== '' && formData.email.trim() !== '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNextStep = () => {
|
|
||||||
if (validateStep1()) setStep(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
setTestimonialError(false);
|
|
||||||
|
|
||||||
if (formData.testimonial.trim() === '') {
|
|
||||||
setTestimonialError(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Testimonial submitted:', formData);
|
|
||||||
setOpen(false);
|
|
||||||
resetForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
setStep(1);
|
|
||||||
setTestimonialError(false);
|
|
||||||
setAvatarPreview(null);
|
|
||||||
setFormData({
|
|
||||||
name: '',
|
|
||||||
email: '',
|
|
||||||
role: '',
|
|
||||||
testimonial: '',
|
|
||||||
});
|
|
||||||
if (fileInputRef.current) fileInputRef.current.value = '';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cn('w-full', className)}>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="relative">
|
|
||||||
{step === 1 && (
|
|
||||||
<div className="w-full">
|
|
||||||
{/* Section header with step info aligned using justify-between */}
|
|
||||||
<div className="flex justify-between items-center pb-3 border-b mb-4">
|
|
||||||
<h3 className="text-base font-semibold">Lengkapi Data Diri</h3>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Langkah 1 dari 2
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex flex-col items-center gap-3 mb-4">
|
|
||||||
<div className="relative">
|
|
||||||
<Avatar className="w-20 h-20 border-2 border-dashed rounded-full">
|
|
||||||
{avatarPreview ? (
|
|
||||||
<AvatarImage
|
|
||||||
src={avatarPreview}
|
|
||||||
alt="Preview"
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<AvatarFallback className="bg-secondary">
|
|
||||||
<LuUser className="w-8 h-8 text-muted-foreground" />
|
|
||||||
</AvatarFallback>
|
|
||||||
)}
|
|
||||||
</Avatar>
|
|
||||||
{avatarPreview && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={removeAvatar}
|
|
||||||
className="absolute -top-1 -right-1 bg-background rounded-full p-1 shadow-md border"
|
|
||||||
aria-label="Hapus foto"
|
|
||||||
>
|
|
||||||
<LuX className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
ref={fileInputRef}
|
|
||||||
className="hidden"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={handleFileChange}
|
|
||||||
aria-label="Unggah foto"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="bordered"
|
|
||||||
className="border-dashed h-8 px-3 text-xs"
|
|
||||||
onClick={triggerFileInput}
|
|
||||||
>
|
|
||||||
<LuUpload className="w-3 h-3 mr-1" />
|
|
||||||
{avatarPreview ? 'Ganti Foto' : 'Unggah Foto'}
|
|
||||||
</Button>
|
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
|
||||||
Maks. 2MB (JPEG, PNG)
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4">
|
|
||||||
{[
|
|
||||||
{
|
|
||||||
id: 'name',
|
|
||||||
label: 'Nama',
|
|
||||||
placeholder: 'Nama lengkap',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'email',
|
|
||||||
label: 'Email',
|
|
||||||
placeholder: 'email@contoh.com',
|
|
||||||
type: 'email',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'role',
|
|
||||||
label: 'Role',
|
|
||||||
placeholder: 'Backend Developer',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
].map((field) => (
|
|
||||||
<div key={field.id} className="space-y-1">
|
|
||||||
<Label htmlFor={field.id} className="text-sm">
|
|
||||||
{field.label}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
id={field.id}
|
|
||||||
type={field.type || 'text'}
|
|
||||||
placeholder={field.placeholder}
|
|
||||||
value={formData[field.id as keyof typeof formData]}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange(field.id, e.target.value)
|
|
||||||
}
|
|
||||||
required={field.required}
|
|
||||||
className="py-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={handleNextStep}
|
|
||||||
disabled={!validateStep1()}
|
|
||||||
>
|
|
||||||
Lanjut
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 2 && (
|
|
||||||
<div className="w-full">
|
|
||||||
{/* Section header with step info aligned using justify-between */}
|
|
||||||
<div className="flex justify-between items-center pb-3 border-b mb-4">
|
|
||||||
<h3 className="text-base font-semibold">Ulasan</h3>
|
|
||||||
<span className="text-sm text-muted-foreground">
|
|
||||||
Langkah 2 dari 2
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Label htmlFor="testimonial" className="text-sm">
|
|
||||||
Testimoni
|
|
||||||
</Label>
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{formData.testimonial.length}/500
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Textarea
|
|
||||||
id="testimonial"
|
|
||||||
placeholder="Bagaimana pengalaman Anda bersama IMPHNEN?"
|
|
||||||
className={cn(
|
|
||||||
'min-h-[140px]',
|
|
||||||
testimonialError && 'border-destructive'
|
|
||||||
)}
|
|
||||||
value={formData.testimonial}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange('testimonial', e.target.value)
|
|
||||||
}
|
|
||||||
maxLength={500}
|
|
||||||
/>
|
|
||||||
{testimonialError && (
|
|
||||||
<div className="text-destructive text-xs mt-1">
|
|
||||||
Harap isi testimonial
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 pt-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="bordered"
|
|
||||||
className="flex-1"
|
|
||||||
onClick={() => setStep(1)}
|
|
||||||
>
|
|
||||||
<LuArrowLeft className="w-4 h-4 mr-1" />
|
|
||||||
Kembali
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" size="lg" className="flex-1">
|
|
||||||
Kirim
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
import TESTIMONIALS from '@/data/testimonials.json';
|
import TESTIMONIALS from '@/data/testimonials.json';
|
||||||
|
import { Button } from '@components';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
import { BsChatLeftQuote } from 'react-icons/bs';
|
import { BsChatLeftQuote } from 'react-icons/bs';
|
||||||
import { FaQuoteLeft } from 'react-icons/fa';
|
import { FaQuoteLeft } from 'react-icons/fa';
|
||||||
import { TestimonialPopup } from './_components/testimonials-popup';
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<div className="py-6 px-4 text-center border-b border-border">
|
<div className="py-16 px-4 text-center border-b border-border">
|
||||||
<BsChatLeftQuote className="size-14 mx-auto my-4 text-foreground" />
|
<BsChatLeftQuote className="size-14 mx-auto my-4 text-foreground" />
|
||||||
<h1 className="text-4xl font-bold mb-4 text-foreground">Testimonial</h1>
|
<h1 className="text-4xl font-bold mb-4 text-foreground">Testimonial</h1>
|
||||||
<p className="max-w-2xl mx-auto text-lg mb-4 text-balance text-muted-foreground">
|
<p className="max-w-2xl mx-auto text-lg mb-8 text-balance text-muted-foreground">
|
||||||
Menampilkan pengalaman dan cerita para member yang telah join ke dalam
|
Menampilkan pengalaman dan cerita para member yang telah join ke dalam
|
||||||
komunitas kami
|
komunitas kami
|
||||||
</p>
|
</p>
|
||||||
<TestimonialPopup />
|
<Link href="/testimonials/submit">
|
||||||
|
<Button>Tulis Testimonialmu</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-8 md:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 my-16 container">
|
<div className="grid gap-8 md:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 my-16 container">
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function Page() {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { baiJamjureeFont } from '@/lib/fonts';
|
import { poppinsFont } from '@/lib/fonts';
|
||||||
import '@/styles/globals.css';
|
import '@/styles/globals.css';
|
||||||
import { cn } from '@utils';
|
import { cn } from '@utils';
|
||||||
import { type Metadata } from 'next';
|
import { type Metadata } from 'next';
|
||||||
@@ -17,7 +17,7 @@ export default function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<html lang="id" suppressHydrationWarning>
|
<html lang="id" suppressHydrationWarning>
|
||||||
<body className={cn(baiJamjureeFont.className, 'antialiased')}>
|
<body className={cn(poppinsFont.className, 'antialiased')}>
|
||||||
<Providers
|
<Providers
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
landing:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/landing.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: landing
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run backoffice:build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=builder /app/dist/apps/backoffice /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run dimentorin:build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=builder /app/dist/apps/dimentorin /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
FROM node:22-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
RUN npm run gacha:build
|
||||||
|
|
||||||
|
FROM nginx:alpine AS runner
|
||||||
|
RUN rm -rf /usr/share/nginx/html/*
|
||||||
|
COPY --from=builder /app/dist/apps/gacha /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:22-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm install
|
||||||
|
RUN npm run landing:build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/dist/apps/landing/.next/standalone .
|
||||||
|
COPY --from=builder /app/dist/apps/landing/public apps/landing/public
|
||||||
|
COPY --from=builder /app/dist/apps/landing/.next/static dist/apps/landing/.next/static
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
ENTRYPOINT ["node", "apps/landing/server.js"]
|
||||||
Generated
-55
@@ -9,7 +9,6 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
@@ -470,33 +469,6 @@
|
|||||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-avatar": {
|
|
||||||
"version": "1.1.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
|
|
||||||
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/react-context": "1.1.2",
|
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
|
||||||
"@radix-ui/react-use-is-hydrated": "0.1.0",
|
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
@@ -830,24 +802,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-use-is-hydrated": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
@@ -2146,15 +2100,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/use-sync-external-store": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vaul": {
|
"node_modules/vaul": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import * as AvatarPrimitive from '@radix-ui/react-avatar';
|
|
||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { cn } from '../lib';
|
|
||||||
|
|
||||||
function Avatar({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Root
|
|
||||||
data-slot="avatar"
|
|
||||||
className={cn(
|
|
||||||
'relative flex size-8 shrink-0 overflow-hidden rounded-full',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarImage({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Image
|
|
||||||
data-slot="avatar-image"
|
|
||||||
className={cn('aspect-square size-full', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function AvatarFallback({
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
||||||
return (
|
|
||||||
<AvatarPrimitive.Fallback
|
|
||||||
data-slot="avatar-fallback"
|
|
||||||
className={cn(
|
|
||||||
'bg-muted flex size-full items-center justify-center rounded-full',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { Avatar, AvatarFallback, AvatarImage };
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
export * from './avatar';
|
|
||||||
export * from './button';
|
export * from './button';
|
||||||
export * from './card';
|
export * from './card';
|
||||||
export * from './dialog';
|
export * from './dialog';
|
||||||
|
|||||||
Generated
-46
@@ -13,7 +13,6 @@
|
|||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@iconify/react": "^6.0.0",
|
"@iconify/react": "^6.0.0",
|
||||||
"@marsidev/react-turnstile": "^1.1.0",
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@redocly/ajv": "^8.11.2",
|
"@redocly/ajv": "^8.11.2",
|
||||||
@@ -7599,33 +7598,6 @@
|
|||||||
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-avatar": {
|
|
||||||
"version": "1.1.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
|
|
||||||
"integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@radix-ui/react-context": "1.1.2",
|
|
||||||
"@radix-ui/react-primitive": "2.1.3",
|
|
||||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
|
||||||
"@radix-ui/react-use-is-hydrated": "0.1.0",
|
|
||||||
"@radix-ui/react-use-layout-effect": "1.1.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"@types/react-dom": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
|
||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@types/react-dom": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
@@ -7959,24 +7931,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@radix-ui/react-use-is-hydrated": {
|
|
||||||
"version": "0.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
|
|
||||||
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"use-sync-external-store": "^1.5.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/react": "*",
|
|
||||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
"@hookform/resolvers": "^5.0.1",
|
"@hookform/resolvers": "^5.0.1",
|
||||||
"@iconify/react": "^6.0.0",
|
"@iconify/react": "^6.0.0",
|
||||||
"@marsidev/react-turnstile": "^1.1.0",
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
"@radix-ui/react-avatar": "^1.1.10",
|
|
||||||
"@radix-ui/react-label": "^2.1.7",
|
"@radix-ui/react-label": "^2.1.7",
|
||||||
"@radix-ui/react-slot": "^1.2.0",
|
"@radix-ui/react-slot": "^1.2.0",
|
||||||
"@redocly/ajv": "^8.11.2",
|
"@redocly/ajv": "^8.11.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user