feat: add Request Feature popup with responsive dialog and drawer components

This commit is contained in:
arraysid
2025-06-01 07:18:33 +07:00
parent 69c934e8c4
commit 98c3c2a326
13 changed files with 1330 additions and 18 deletions
@@ -1,13 +1,9 @@
'use client';
import { motion } from 'framer-motion';
import { FiPlus } from 'react-icons/fi';
import { RequestFeaturePopup } from './request-feature-popup';
export function FeatureRequestCTA() {
const handleSubmitFeature = () => {
alert('Feature submission form would appear here!');
};
return (
<motion.div
initial={{ opacity: 0, y: -20 }}
@@ -33,18 +29,7 @@ export function FeatureRequestCTA() {
Bagikan ide fitur atau layanan yang ingin Anda lihat di komunitas. Ide
terbaik dengan vote tertinggi akan kami prioritaskan!
</motion.p>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={handleSubmitFeature}
className="bg-white text-primary-500 font-semibold py-3 px-8 rounded-full flex items-center justify-center gap-2 mx-auto shadow-lg hover:shadow-xl transition-all"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
>
<FiPlus className="w-5 h-5" />
<span>Ajukan Fitur</span>
</motion.button>
<RequestFeaturePopup />
</div>
</motion.div>
);
@@ -9,6 +9,12 @@ import { MdOutlineOpenInNew } from 'react-icons/md';
export default function ProjectsVote() {
const [upcomingItems, setUpcomingItems] = useState([
{
title: 'IMPHNEN Project Showcase',
description: 'Showcase projectmu ke member lain dan dapatkan feedback',
votes: 42,
voted: false,
},
{
title: 'IMPHNEN Meme Generator',
description: 'Bikin meme kocak kapanpun dengan mudah',
@@ -0,0 +1,130 @@
'use client';
import * as React from 'react';
import { useMediaQuery } from '@/hooks/use-media-query';
import {
Button,
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
Input,
Label,
Textarea,
} from '@components';
import { cn } from '@utils';
export function RequestFeaturePopup() {
const [open, setOpen] = React.useState(false);
const isDesktop = useMediaQuery('(min-width: 768px)');
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="secondary">Request Fitur</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle className="text-2xl">Request Fitur</DialogTitle>
<DialogDescription>
Request fitur apa pun disini. Kami akan mempertimbangkan
permintaan Anda.
</DialogDescription>
</DialogHeader>
<ProfileForm />
</DialogContent>
</Dialog>
);
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button variant="secondary">Request Fitur</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>Request Fitur</DrawerTitle>
<DrawerDescription>
Punya ide fitur keren? Bagikan di sini! Kami selalu terbuka untuk
inovasi yang bisa membuat produk ini lebih baik.
</DrawerDescription>
</DrawerHeader>
<ProfileForm className="px-4" />
<DrawerFooter className="pt-2">
<DrawerClose asChild>
<Button variant="bordered">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
interface ProfileFormProps extends React.ComponentProps<'form'> {
showFooterButton?: boolean;
}
function ProfileForm({
className,
showFooterButton = false,
}: ProfileFormProps) {
const [description, setDescription] = React.useState('');
const maxDescriptionLength = 10000;
return (
<form className={cn('grid items-start gap-8', className)}>
<div className="grid gap-4">
<Label htmlFor="namaFitur" className="font-medium">
Nama Fitur *
</Label>
<Input
id="namaFitur"
placeholder="Masukkan nama fitur yang diinginkan"
className="h-11"
/>
</div>
<div className="grid gap-4">
<div className="flex justify-between items-center">
<Label htmlFor="deskripsiFitur" className="font-medium">
Deskripsi Fitur *
</Label>
<span className="text-sm text-muted-foreground">
{description.length}/{maxDescriptionLength}
</span>
</div>
<Textarea
id="deskripsiFitur"
placeholder="Jelaskan fitur yang Anda inginkan secara detail..."
maxLength={maxDescriptionLength}
rows={6}
className="min-h-[150px]"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<p className="text-sm text-muted-foreground -mt-2">
Jelaskan manfaat dan cara kerja fitur yang Anda inginkan
</p>
</div>
{!showFooterButton && (
<Button type="submit" size="lg" className="mt-2">
Kirim Permintaan
</Button>
)}
</form>
);
}
@@ -3,7 +3,7 @@ import ProjectsVote from './_components/projects-vote';
export default function Page() {
return (
<div className="p-4 md:p-6 bg-gray-50 min-h-screen">
<div className="pt-4 md:pt-6 pb-56 bg-gray-50 min-h-screen">
<FeatureRequestCTA />
<ProjectsVote />
</div>
+33
View File
@@ -0,0 +1,33 @@
'use client';
import { useEffect, useState } from 'react';
export function useMediaQuery(query: string): boolean {
// 1) Always default to `false`—this guarantees SSR and the very first client render match.
const [matches, setMatches] = useState(false);
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
const mediaQueryList = window.matchMedia(query);
// 2) On mount, immediately set to the real match value (true or false).
setMatches(mediaQueryList.matches);
// 3) Subscribe to changes.
const listener = (event: MediaQueryListEvent) => {
setMatches(event.matches);
};
mediaQueryList.addEventListener('change', listener);
return () => {
mediaQueryList.removeEventListener('change', listener);
};
}, [query]);
return matches;
}
export default useMediaQuery;