Skip to content

Commit

Permalink
Merge pull request #41 from and-voila/srizvi/issue24
Browse files Browse the repository at this point in the history
chore: migrate components and hooks
  • Loading branch information
srizvi authored Nov 10, 2023
2 parents b91639a + a52548c commit fdf2802
Show file tree
Hide file tree
Showing 65 changed files with 4,779 additions and 7 deletions.
40 changes: 40 additions & 0 deletions app/components/banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { cva, VariantProps } from 'class-variance-authority';

import { Icons } from '@/app/components/shared/icons';

import { cn } from '../lib/utils';

const bannerVariants = cva(
'border-2 text-center p-4 text-base flex items-center w-full',
{
variants: {
variant: {
warning: 'bg-yellow-400 border-yellow-600 text-black',
success: 'bg-alternate border-green-600 text-white',
},
},
defaultVariants: {
variant: 'warning',
},
},
);

interface BannerProps extends VariantProps<typeof bannerVariants> {
label: string;
}

const iconMap = {
warning: Icons.warning,
success: Icons.circleChecked,
};

export const Banner = ({ label, variant }: BannerProps) => {
const Icon = iconMap[variant || 'warning'];

return (
<div className={cn(bannerVariants({ variant }))}>
<Icon className="mr-2 h-6 w-6" />
{label}
</div>
);
};
22 changes: 22 additions & 0 deletions app/components/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ElementType, ReactNode } from 'react';
import clsx from 'clsx';

interface ContainerProps {
as?: ElementType;
className?: string;
children?: ReactNode;
}

export function Container({
as: Component = 'div',
className,
children,
}: ContainerProps) {
return (
<Component
className={clsx('mx-auto max-w-5xl px-6 lg:px-8 xl:px-10', className)}
>
<div className="mx-auto max-w-2xl lg:max-w-none">{children}</div>
</Component>
);
}
71 changes: 71 additions & 0 deletions app/components/free-counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
'use client';

import { useEffect, useState } from 'react';
import { MAX_FREE_TOKENS } from '@/constants';

import { Icons } from '@/app/components/shared/icons';
import { Button } from '@/app/components/ui/button';
import { Card, CardContent } from '@/app/components/ui/card';
import { useProModal } from '@/app/hooks/use-pro-modal';
import { isTeacher } from '@/app/lib/teacher';

interface FreeCounterProps {
apiLimitCount: number;
isPaidMember: boolean;
userId: string;
}

export const FreeCounter = ({
apiLimitCount = 0,
isPaidMember = false,
userId,
}: FreeCounterProps) => {
const proModal = useProModal();
const [mounted, setMounted] = useState(false);

useEffect(() => {
setMounted(true);
}, []);

if (!mounted) {
return null;
}

if (isPaidMember) {
return null;
}

if (isTeacher(userId)) {
return null;
}

return (
<div className="px-2">
<Card className="border bg-primary-foreground">
<CardContent className="py-4">
<div className="mb-4 space-y-2 text-center text-xs text-foreground">
<h2 className="font-display text-lg uppercase text-foreground">
Get Early Access
</h2>
<p className="text-muted-foreground">
You&apos;re on the free Good plan. Upgrade to the Best plan for
some real magic.
</p>
{/*<p>
You&apos;ve used {apiLimitCount} / {MAX_FREE_TOKENS} AI tokens.
</p>
<Progress
value={(apiLimitCount / MAX_FREE_TOKENS) * 100}
className="h3"
/>*/}
</div>
<Button onClick={proModal.onOpen} className="w-full" variant="custom">
Upgrade
<Icons.magic className="ml-2 h-4 w-4" />
</Button>
</CardContent>
</Card>
</div>
);
};
54 changes: 54 additions & 0 deletions app/components/icon-badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { cva, VariantProps } from 'class-variance-authority';

import { IconName, Icons } from '@/app/components/shared/icons';
import { cn } from '@/app/lib/utils';

const backgroundVariants = cva('rounded-xl flex items-center justify-center', {
variants: {
variant: {
default: 'bg-transparent',
success: 'bg-emerald-100',
},
size: {
default: 'p-2',
sm: 'p-1',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
});

const iconVariants = cva('', {
variants: {
variant: {
default: 'text-brand',
success: 'text-emerald-700',
},
size: {
default: 'h-6 w-6',
sm: 'h-4 w-4',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
});

type BackgroundVariantsProps = VariantProps<typeof backgroundVariants>;
type IconVariantsProps = VariantProps<typeof iconVariants>;

interface IconBadgeProps extends BackgroundVariantsProps, IconVariantsProps {
icon: IconName;
}

export const IconBadge = ({ icon, variant, size }: IconBadgeProps) => {
const Icon = Icons[icon];
return (
<div className={cn(backgroundVariants({ variant, size }))}>
<Icon className={cn(iconVariants({ variant, size }))} />
</div>
);
};
83 changes: 83 additions & 0 deletions app/components/learn/courses/course-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Suspense } from 'react';
import Image from 'next/image';
import Link from 'next/link';

import { Skeleton } from '@/app/components/ui/skeleton';
import { getCoursePrice } from '@/app/lib/course-pricing';

interface CourseCardProps {
id: string;
title: string;
preview: string;
imageUrl: string;
displayImage?: boolean;
price: number;
progress: number | null;
category: string;
isPaidMember: boolean;
purchased: boolean;
}
export const CourseCard = ({
id,
title,
preview,
imageUrl,
displayImage = true,
price,
progress,
category,
isPaidMember,
purchased,
}: CourseCardProps) => {
const displayPrice = getCoursePrice(price, isPaidMember, purchased);

return (
<Link href={`/learn/courses/${id}`}>
<div className="group h-full overflow-hidden rounded-xl border bg-white transition hover:shadow-sm dark:bg-background">
{displayImage && (
<div className="relative aspect-video w-full overflow-hidden md:grayscale md:group-hover:grayscale-0">
<Suspense
fallback={
<Skeleton className="relative aspect-video h-32 w-full" />
}
>
<Image fill className="object-cover" alt={title} src={imageUrl} />
</Suspense>
</div>
)}
<div className="mt-1 flex flex-col p-4">
<div className="mb-2 flex items-center justify-between">
<Suspense fallback={<Skeleton className="h-4 w-12" />}>
<p className="font-mono text-sm text-muted-foreground">
{category}
</p>
</Suspense>
<Suspense fallback={<Skeleton className="h-4 w-12" />}>
{progress !== null ? (
progress === 0 ? (
<p className="font-mono text-sm text-brand">Not Started</p>
) : progress === 100 ? (
<p className="font-mono text-sm text-alternate">Complete</p>
) : (
<p className="font-mono text-sm text-brand">In Progress</p>
)
) : (
<p className="font-mono text-sm text-brand">{displayPrice}</p>
)}
</Suspense>
</div>
<Suspense fallback={<Skeleton className="h-8 w-3/4" />}>
<div className="line-clamp-2 text-lg font-semibold leading-tight transition group-hover:text-brand">
{title}
</div>
</Suspense>
<Suspense fallback={<Skeleton className="h-24 w-3/5" />}>
<p className="my-2 line-clamp-2 text-sm text-muted-foreground">
{preview}
</p>
</Suspense>
</div>
</div>
</Link>
);
};
51 changes: 51 additions & 0 deletions app/components/learn/courses/course-enroll-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client';

import { useState } from 'react';
import axios from 'axios';

import { Button } from '@/app/components/ui/button';
import { toast } from '@/app/components/ui/use-toast';
import { formatPrice } from '@/app/lib/format';

interface CourseEnrollButtonProps {
price: number;
courseId: string;
}

export const CourseEnrollButton = ({
price,
courseId,
}: CourseEnrollButtonProps) => {
const [isLoading, setIsLoading] = useState(false);

const onClick = async () => {
try {
setIsLoading(true);

const response = await axios.post(`/api/courses/${courseId}/checkout`);

window.location.assign(response.data.url);
} catch {
toast({
title: 'Uh oh! An error occurred.',
description:
'Honestly, we have no idea what happened. Please try again.',
variant: 'destructive',
});
} finally {
setIsLoading(false);
}
};

return (
<Button
variant="secondary"
onClick={onClick}
disabled={isLoading}
size="sm"
className="w-full flex-shrink-0 lg:w-auto"
>
Buy for {formatPrice(price)}
</Button>
);
};
28 changes: 28 additions & 0 deletions app/components/learn/courses/course-mobile-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Icons } from '@/app/components/shared/icons';
import { Sheet, SheetContent, SheetTrigger } from '@/app/components/ui/sheet';
import { CourseMobileSidebarProps } from '@/app/lib/types';

import { CourseSidebar } from './course-sidebar';

export const CourseMobileSidebar = ({
course,
progressCount,
isPaidMember,
apiLimitCount,
}: CourseMobileSidebarProps) => {
return (
<Sheet>
<SheetTrigger className="pr-4 transition hover:opacity-75 md:hidden">
<Icons.hamburgerMenu />
</SheetTrigger>
<SheetContent side="left" className="w-72 bg-white p-0">
<CourseSidebar
course={course}
progressCount={progressCount}
isPaidMember={isPaidMember}
apiLimitCount={apiLimitCount}
/>
</SheetContent>
</Sheet>
);
};
38 changes: 38 additions & 0 deletions app/components/learn/courses/course-navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { redirect } from 'next/navigation';

import { UserAccountNav } from '@/app/components/layout/user-account-nav';
import { CourseMobileSidebar } from '@/app/components/learn/courses/course-mobile-sidebar';
import { NavbarRoutes } from '@/app/config/navbar-routes';
import { getCurrentUser } from '@/app/lib/session';
import { CourseNavbarProps } from '@/app/lib/types';

export const CourseNavbar = async ({
course,
progressCount,
isPaidMember,
apiLimitCount,
}: CourseNavbarProps) => {
const user = await getCurrentUser();
const userId = user?.id;
if (!userId) {
return redirect('/login');
}
return (
<div className="flex h-full items-center border-b bg-[#dcdfe5] p-4 shadow-sm dark:bg-[#16161a]">
<CourseMobileSidebar
course={course}
progressCount={progressCount}
isPaidMember={isPaidMember}
apiLimitCount={apiLimitCount}
/>
<NavbarRoutes userId={userId} />
<UserAccountNav
user={{
name: user.name,
image: user.image,
email: user.email,
}}
/>
</div>
);
};
Loading

0 comments on commit fdf2802

Please sign in to comment.