feat(ui): add avatar and badge components

This commit is contained in:
shafin-r
2025-07-10 14:51:45 +06:00
parent d42a42a8d1
commit 64fc4d9a9a
12 changed files with 597 additions and 256 deletions

View File

@ -5,6 +5,7 @@ import { ChevronLeft, Layers } from "lucide-react";
import { useTimer } from "@/context/TimerContext";
import styles from "@/css/Header.module.css";
import { useExam } from "@/context/ExamContext";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
const API_URL = "https://examjam-api.pptx704.com";
@ -101,15 +102,11 @@ const Header = ({
<header className={styles.header}>
{displayUser && (
<div className={styles.profile}>
{image && (
<Image
src={image}
alt="Profile"
width={40}
height={40}
className={styles.profileImg}
/>
)}
<Avatar className="bg-gray-200 w-10 h-10">
<AvatarFallback className=" text-lg">
{userData?.name ? userData.name.charAt(0).toUpperCase() : ""}
</AvatarFallback>
</Avatar>
<span className={styles.text}>
Hello {userData?.name ? userData.name.split(" ")[0] : ""}
</span>

View File

@ -3,115 +3,158 @@ import Link from "next/link";
import Image from "next/image";
import styles from "../css/SlidingGallery.module.css";
const views = [
{
id: "1",
content: (
<Link
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
className={styles.link}
>
<div className={styles.facebook}>
<div className={styles.textView}>
<h3 className={styles.facebookOne}>Meet, Share, and Learn!</h3>
<p className={styles.facebookTwo}>Join Facebook Community</p>
</div>
<div className={styles.logoView}>
<Image
src="/images/static/facebook-logo.png"
alt="Facebook Logo"
width={120}
height={120}
/>
</div>
</div>
</Link>
),
},
{
id: "2",
content: (
<Link
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
className={styles.link}
>
<div className={styles.facebook}>
<div className={styles.textView}>
<h3 className={styles.facebookOne}>Meet, Share, and Learn!</h3>
<p className={styles.facebookTwo}>Join Facebook Community</p>
</div>
<div className={styles.logoView}>
<Image
src="/images/static/facebook-logo.png"
alt="Facebook Logo"
width={120}
height={120}
/>
</div>
</div>
</Link>
),
},
{
id: "3",
content: (
<Link
href="https://www.facebook.com/share/g/15jdqESvWV/?mibextid=wwXIfr"
className={styles.link}
>
<div className={styles.facebook}>
<div className={styles.textView}>
<h3 className={styles.facebookOne}>Meet, Share, and Learn!</h3>
<p className={styles.facebookTwo}>Join Facebook Community</p>
</div>
<div className={styles.logoView}>
<Image
src="/images/static/facebook-logo.png"
alt="Facebook Logo"
width={120}
height={120}
/>
</div>
</div>
</Link>
),
},
];
const SlidingGallery = () => {
const SlidingGallery = ({
views = [],
className = "",
showPagination = true,
autoScroll = false,
autoScrollInterval = 5000,
onSlideChange = () => {},
height = "100vh",
}) => {
const [activeIdx, setActiveIdx] = useState(0);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const scrollRef = useRef(null);
const galleryRef = useRef(null);
const autoScrollRef = useRef(null);
const handleScroll = (event) => {
const scrollLeft = event.target.scrollLeft;
const slideWidth = event.target.clientWidth;
const index = Math.round(scrollLeft / slideWidth);
setActiveIdx(index);
// Auto-scroll functionality
useEffect(() => {
if (autoScroll && views.length > 1) {
autoScrollRef.current = setInterval(() => {
setActiveIdx((prevIdx) => {
const nextIdx = (prevIdx + 1) % views.length;
goToSlide(nextIdx);
return nextIdx;
});
}, autoScrollInterval);
return () => {
if (autoScrollRef.current) {
clearInterval(autoScrollRef.current);
}
};
}
}, [autoScroll, autoScrollInterval, views.length]);
// Clear auto-scroll on user interaction
const handleUserInteraction = () => {
if (autoScrollRef.current) {
clearInterval(autoScrollRef.current);
}
};
useEffect(() => {
const updateDimensions = () => {
if (galleryRef.current) {
setDimensions({
width: galleryRef.current.clientWidth,
height: galleryRef.current.clientHeight,
});
}
};
// Initial dimension update
updateDimensions();
// Add resize listener
window.addEventListener("resize", updateDimensions);
// Cleanup
return () => window.removeEventListener("resize", updateDimensions);
}, []);
useEffect(() => {
// Recalculate active index when dimensions change
if (scrollRef.current && dimensions.width > 0) {
const scrollLeft = scrollRef.current.scrollLeft;
const slideWidth = dimensions.width;
const index = Math.round(scrollLeft / slideWidth);
setActiveIdx(index);
}
}, [dimensions]);
const handleScroll = (event) => {
handleUserInteraction();
const scrollLeft = event.target.scrollLeft;
const slideWidth = dimensions.width;
const index = Math.round(scrollLeft / slideWidth);
if (index !== activeIdx) {
setActiveIdx(index);
onSlideChange(index);
}
};
const goToSlide = (index) => {
if (scrollRef.current) {
scrollRef.current.scrollTo({
left: index * dimensions.width,
behavior: "smooth",
});
}
};
const handleDotClick = (index) => {
handleUserInteraction();
goToSlide(index);
setActiveIdx(index);
onSlideChange(index);
};
// Early return if no views
if (!views || views.length === 0) {
return (
<div className={`${styles.gallery} ${className}`}>
<div className={styles.emptyState}>
<p>No content to display</p>
</div>
</div>
);
}
return (
<div className={styles.gallery}>
<div
className={`${styles.gallery} ${className}`}
ref={galleryRef}
style={{ height }}
>
<div
className={styles.scrollContainer}
ref={scrollRef}
onScroll={handleScroll}
style={{
width: "100%",
height: "100%",
}}
>
{views.map((item) => (
<div key={item.id} className={styles.slide}>
<div
key={item.id}
className={styles.slide}
style={{
width: dimensions.width,
height: "100%",
flexShrink: 0,
}}
>
{item.content}
</div>
))}
</div>
<div className={styles.pagination}>
{views.map((_, index) => (
<div
key={index}
className={`${styles.dot} ${
activeIdx === index ? styles.activeDot : styles.inactiveDot
}`}
/>
))}
</div>
{showPagination && views.length > 1 && (
<div className={styles.pagination}>
{views.map((_, index) => (
<div
key={index}
className={`${styles.dot} ${
activeIdx === index ? styles.activeDot : styles.inactiveDot
}`}
onClick={() => handleDotClick(index)}
style={{ cursor: "pointer" }}
/>
))}
</div>
)}
</div>
);
};

53
components/ui/avatar.tsx Normal file
View File

@ -0,0 +1,53 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
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, AvatarImage, AvatarFallback }

46
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }