Skip to content

Commit

Permalink
feat: ✨ created migrationis and sidebar
Browse files Browse the repository at this point in the history
  • Loading branch information
esbnet committed Mar 5, 2024
1 parent 176f8c8 commit ac0238d
Show file tree
Hide file tree
Showing 37 changed files with 1,687 additions and 21 deletions.
17 changes: 17 additions & 0 deletions .env.semple
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=

NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

OPENAI_API_KEY=
REPLICATE_API_TOKEN=

DATABASE_URL=

STRIPE_API_KEY=
STRIPE_WEBHOOK_SECRET=

NEXT_PUBLIC_APP_URL="http://localhost:3000"
4 changes: 2 additions & 2 deletions app/(auth)/(routes)/sign-in/[[...sign-in]]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SignUp } from "@clerk/nextjs";
import { SignIn } from "@clerk/nextjs";

export default function Page() {
return <SignUp />;
return <SignIn />;
}
26 changes: 26 additions & 0 deletions app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Navbar from "@/components/navbar";
import { Sidebar } from "@/components/sidebar";
import { getApiLimitCount } from "@/lib/api-limit";
import { checkSubscription } from "@/lib/subscription";
import { ReactNode } from "react";

export default async function DashboardLayout({
children,
}: {
children: ReactNode;
}) {
const apiLimitCount = await getApiLimitCount();
const isPro = await checkSubscription();

return (
<div className="h-full relative">
<div className="hidden h-full md:flex md:w-72 md:flex-col md:fixed md:inset-y-0 z-80 bg-gray-900">
<Sidebar isPro={isPro} apiLimitCount={apiLimitCount} />
</div>
<main className="md:pl-72 pb-10">
<Navbar />
{children}
</main>
</div>
);
}
13 changes: 13 additions & 0 deletions app/hooks/use-pro-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { create } from "zustand";

interface useProModalStore {
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}

export const useProModal = create<useProModalStore>((set) => ({
isOpen: false,
onOpen: () => set({ isOpen: true }),
onClose: () => set({ isOpen: false }),
}));
57 changes: 57 additions & 0 deletions components/free-counter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Zap } from "lucide-react";
import { useEffect, useState } from "react";

import { useProModal } from "@/app/hooks/use-pro-modal";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { MAX_FREE_COUNTS } from "@/constants";

export const FreeCounter = ({
isPro = false,
apiLimitCount = 0,
}: {
isPro: boolean;
apiLimitCount: number;
}) => {
const [mounted, setMounted] = useState(false);
const proModal = useProModal();

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

if (!mounted) {
return null;
}

if (isPro) {
return null;
}

return (
<div className="px-3">
<Card className="bg-white/10 border-0">
<CardContent className="py-6">
<div className="text-center text-sm text-white mb-4 space-y-2">
<p>
{apiLimitCount} / {MAX_FREE_COUNTS} Free Generations
</p>
<Progress
className="h-3"
value={(apiLimitCount / MAX_FREE_COUNTS) * 100}
/>
</div>
<Button
onClick={proModal.onOpen}
variant="premium"
className="w-full"
>
Upgrade
<Zap className="w-4 h-4 ml-2 fill-white" />
</Button>
</CardContent>
</Card>
</div>
);
};
39 changes: 39 additions & 0 deletions components/mobile-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { Menu } from "lucide-react";
import { useEffect, useState } from "react";

import { Sidebar } from "@/components/sidebar";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";

export const MobileSidebar = ({
apiLimitCount = 0,
isPro = false,
}: {
apiLimitCount: number;
isPro: boolean;
}) => {
const [isMounted, setIsMounted] = useState(false);

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

if (!isMounted) {
return null;
}

return (
<Sheet>
<SheetTrigger>
<Button variant="ghost" size="icon" className="md:hidden">
<Menu />
</Button>
</SheetTrigger>
<SheetContent side="left" className="p-0">
<Sidebar isPro={isPro} apiLimitCount={apiLimitCount} />
</SheetContent>
</Sheet>
);
};
19 changes: 19 additions & 0 deletions components/navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { UserButton } from "@clerk/nextjs";

import { checkSubscription } from "@/lib/subscription";
import { getApiLimitCount } from "../lib/api-limit";
import { MobileSidebar } from "./mobile-sidebar";

export default async function Navbar() {
const apiLimitCount = await getApiLimitCount();
const isPro = await checkSubscription();

return (
<div className="flex items-center p-4">
<MobileSidebar isPro={isPro} apiLimitCount={apiLimitCount} />
<div className="flex w-full justify-end">
<UserButton afterSignOutUrl="/" />
</div>
</div>
);
}
85 changes: 85 additions & 0 deletions components/pro-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use client";

import axios from "axios";
import { Check, Zap } from "lucide-react";
import { useState } from "react";
import { toast } from "react-hot-toast";

import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { tools } from "@/constants";
// import { useProModal } from "@/hooks/use-pro-modal";
import { cn } from "@/lib/utils";
import { useProModal } from "../hooks/use-pro-modal";

export const ProModal = () => {
const proModal = useProModal();
const [loading, setLoading] = useState(false);

const onSubscribe = async () => {
try {
setLoading(true);
const response = await axios.get("/api/stripe");

window.location.href = response.data.url;
} catch (error) {
toast.error("Something went wrong");
} finally {
setLoading(false);
}
};

return (
<Dialog open={proModal.isOpen} onOpenChange={proModal.onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex justify-center items-center flex-col gap-y-4 pb-2">
<div className="flex items-center gap-x-2 font-bold text-xl">
Upgrade to Genius
<Badge variant="premium" className="uppercase text-sm py-1">
pro
</Badge>
</div>
</DialogTitle>
<DialogDescription className="text-center pt-2 space-y-2 text-zinc-900 font-medium">
{tools.map((tool) => (
<Card
key={tool.href}
className="p-3 border-black/5 flex items-center justify-between"
>
<div className="flex items-center gap-x-4">
<div className={cn("p-2 w-fit rounded-md", tool.bgColor)}>
<tool.icon className={cn("w-6 h-6", tool.color)} />
</div>
<div className="font-semibold text-sm">{tool.label}</div>
</div>
<Check className="text-primary w-5 h-5" />
</Card>
))}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
disabled={loading}
onClick={onSubscribe}
size="lg"
variant="premium"
className="w-full"
>
Upgrade
<Zap className="w-4 h-4 ml-2 fill-white" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
109 changes: 109 additions & 0 deletions components/sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

import {
Code,
ImageIcon,
LayoutDashboard,
MessageSquare,
Music,
Settings,
VideoIcon,
} from "lucide-react";
import { Montserrat } from "next/font/google";
import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";

import { cn } from "../lib/utils";
import { FreeCounter } from "./free-counter";

const poppins = Montserrat({ weight: "600", subsets: ["latin"] });

const routes = [
{
label: "Dashboard",
icon: LayoutDashboard,
href: "/dashboard",
color: "text-sky-500",
},
{
label: "Conversation",
icon: MessageSquare,
href: "/conversation",
color: "text-violet-500",
},
{
label: "Image Generation",
icon: ImageIcon,
color: "text-pink-700",
href: "/image",
},
{
label: "Video Generation",
icon: VideoIcon,
color: "text-orange-700",
href: "/video",
},
{
label: "Music Generation",
icon: Music,
color: "text-emerald-500",
href: "/music",
},
{
label: "Code Generation",
icon: Code,
color: "text-green-700",
href: "/code",
},
{
label: "Settings",
icon: Settings,
href: "/settings",
},
];

export const Sidebar = ({
apiLimitCount = 0,
isPro = false,
}: {
apiLimitCount: number;
isPro: boolean;
}) => {
const pathname = usePathname();

return (
<div className="space-y-4 py-4 flex flex-col h-full bg-[#111827] text-white">
<div className="px-3 py-2 flex-1">
<Link href="/dashboard" className="flex items-center pl-3 mb-14">
<div className="relative h-8 w-8 mr-4">
<Image fill alt="Logo" src="/logo.png" />
</div>
<h1 className={cn("text-2xl font-bold", poppins.className)}>
Genius
</h1>
</Link>
<div className="space-y-1">
{routes.map((route) => (
<Link
key={route.href}
href={route.href}
className={cn(
"text-sm group flex p-3 w-full justify-start font-medium cursor-pointer hover:text-white hover:bg-white/10 rounded-lg transition",
pathname === route.href
? "text-white bg-white/10"
: "text-zinc-400"
)}
>
<div className="flex items-center flex-1">
<route.icon className={cn("h-5 w-5 mr-3", route.color)} />
{route.label}
</div>
</Link>
))}
</div>
</div>
<FreeCounter apiLimitCount={apiLimitCount} isPro={isPro} />
</div>
);
};
Loading

0 comments on commit ac0238d

Please sign in to comment.