mirror of
https://github.com/ahmadk953/tasko.git
synced 2025-05-01 03:09:34 +00:00
Initial Commit
This commit is contained in:
commit
f3e2f01bd7
150 changed files with 13612 additions and 0 deletions
|
@ -0,0 +1,90 @@
|
|||
import Link from "next/link";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
import { redirect } from "next/navigation";
|
||||
import { HelpCircle, User2 } from "lucide-react";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { Hint } from "@/components/hint";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { FormPopover } from "@/components/form/form-popover";
|
||||
import { MAX_FREE_BOARDS } from "@/constants/boards";
|
||||
import { getAvailableCount } from "@/lib/org-limit";
|
||||
import { checkSubscription } from "@/lib/subscription";
|
||||
|
||||
export const BoardList = async () => {
|
||||
const { orgId } = auth();
|
||||
|
||||
if (!orgId) {
|
||||
return redirect("/select-org");
|
||||
}
|
||||
|
||||
const boards = await db.board.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
const availableCount = await getAvailableCount();
|
||||
const isPro = await checkSubscription();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center font-semibold text-lg text-neutral-700">
|
||||
<User2 className="h-6 w-6 mr-2" />
|
||||
Your boards
|
||||
</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{boards.map((board) => (
|
||||
<Link
|
||||
key={board.id}
|
||||
href={`/board/${board.id}`}
|
||||
className="group relative aspect-video bg-no-repeat bg-center bg-cover bg-sky-700 rounded-sm h-full w-full p-2 overflow-hidden"
|
||||
style={{ backgroundImage: `url(${board.imageThumbUrl})` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/30 group-hover:bg-black/40 transition" />
|
||||
<p className="relative font-semibold text-white">{board.title}</p>
|
||||
</Link>
|
||||
))}
|
||||
<FormPopover sideOffset={10} side="right">
|
||||
<div
|
||||
role="button"
|
||||
className="aspect-video relative h-full w-full bg-muted rounded-sm flex flex-col gap-y-1 items-center justify-center hover:opacity-75 transition"
|
||||
>
|
||||
<p className="text-sm">Create new board</p>
|
||||
<span className="text-xs">
|
||||
{isPro
|
||||
? "Unlimited"
|
||||
: `${MAX_FREE_BOARDS - availableCount} remaining`}
|
||||
</span>
|
||||
<Hint
|
||||
sideOffset={40}
|
||||
description={`
|
||||
Free Workspaces can have up to 5 open boards. For unlimited boards upgrade this workspace.
|
||||
`}
|
||||
>
|
||||
<HelpCircle className="absolute bottom-2 right-2 h-[14px] w-[14px]" />
|
||||
</Hint>
|
||||
</div>
|
||||
</FormPopover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
BoardList.Skeleton = function SkeletonBoardList() {
|
||||
return (
|
||||
<div className="grid gird-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<Skeleton className="aspect-video h-full w-full p-2" />
|
||||
<Skeleton className="aspect-video h-full w-full p-2" />
|
||||
<Skeleton className="aspect-video h-full w-full p-2" />
|
||||
<Skeleton className="aspect-video h-full w-full p-2" />
|
||||
<Skeleton className="aspect-video h-full w-full p-2" />
|
||||
<Skeleton className="aspect-video h-full w-full p-2" />
|
||||
<Skeleton className="aspect-video h-full w-full p-2" />
|
||||
<Skeleton className="aspect-video h-full w-full p-2" />
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { CreditCard } from "lucide-react";
|
||||
|
||||
import { useOrganization } from "@clerk/nextjs";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
interface InfoProps {
|
||||
isPro: boolean;
|
||||
}
|
||||
|
||||
export const Info = ({ isPro }: InfoProps) => {
|
||||
const { organization, isLoaded } = useOrganization();
|
||||
|
||||
if (!isLoaded) {
|
||||
return <Info.Skeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="w-[60px] h-[60px] relative">
|
||||
<Image
|
||||
fill
|
||||
src={organization?.imageUrl!}
|
||||
alt="Organization Logo"
|
||||
className="rounded-md object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="font-semibold text-xl">{organization?.name}</p>
|
||||
<div className="flex items-center text-xs text-muted-foreground">
|
||||
<CreditCard className="h-3 w-3 mr-1" />
|
||||
{isPro ? "Pro" : "Free"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Info.Skeleton = function SkeletonInfo() {
|
||||
return (
|
||||
<div className="flex items-center gap-x-4">
|
||||
<div className="w-[60px] h-[60px] relative">
|
||||
<Skeleton className="w-full h-full absolute" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-10 w-[200px]" />
|
||||
<div className="flex items-center">
|
||||
<Skeleton className="h-4 w-4 mr-2" />
|
||||
<Skeleton className="h-4 w-[100px]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useOrganizationList } from "@clerk/nextjs";
|
||||
|
||||
export const OrgControl = () => {
|
||||
const params = useParams();
|
||||
const { setActive } = useOrganizationList();
|
||||
|
||||
useEffect(() => {
|
||||
if (!setActive) return;
|
||||
|
||||
setActive({
|
||||
organization: params.organizationId as string,
|
||||
});
|
||||
}, [setActive, params.organizationId]);
|
||||
|
||||
return null;
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
import { auth } from "@clerk/nextjs";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { db } from "@/lib/db";
|
||||
import { ActivityItem } from "@/components/activity-item";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
export const ActivityList = async () => {
|
||||
const { orgId } = auth();
|
||||
|
||||
if (!orgId) redirect("/select-org");
|
||||
|
||||
const auditLogs = await db.auditLog.findMany({
|
||||
where: {
|
||||
orgId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ol className="space-y-4 mt-4">
|
||||
<p className="hidden last:block text-xs text-center text-muted-foreground">
|
||||
No activity found inside this organization
|
||||
</p>
|
||||
{auditLogs.map((log) => (
|
||||
<ActivityItem key={log.id} data={log} />
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
};
|
||||
|
||||
ActivityList.Skeleton = function ActivityListSkeleton() {
|
||||
return (
|
||||
<ol className="space-y-4 mt-4">
|
||||
<Skeleton className="w-[80%] h-14" />
|
||||
<Skeleton className="w-[50%] h-14" />
|
||||
<Skeleton className="w-[70%] h-14" />
|
||||
<Skeleton className="w-[80%] h-14" />
|
||||
<Skeleton className="w-[75%] h-14" />
|
||||
</ol>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,23 @@
|
|||
import { Suspense } from "react";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
import { Info } from "../_components/info";
|
||||
import { ActivityList } from "./_components/activity-list";
|
||||
import { checkSubscription } from "@/lib/subscription";
|
||||
|
||||
const ActivityPage = async () => {
|
||||
const isPro = await checkSubscription();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Info isPro={isPro} />
|
||||
<Separator className="my-2" />
|
||||
<Suspense fallback={<ActivityList.Skeleton />}>
|
||||
<ActivityList />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActivityPage;
|
|
@ -0,0 +1,39 @@
|
|||
"use client";
|
||||
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { stripeRedirect } from "@/actions/stripe-redirect";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useAction } from "@/hooks/use-action";
|
||||
import { useProModal } from "@/hooks/use-pro-modal";
|
||||
|
||||
interface SubscriptionButtonProps {
|
||||
isPro: boolean;
|
||||
}
|
||||
|
||||
export const SubscriptionButton = ({ isPro }: SubscriptionButtonProps) => {
|
||||
const proModal = useProModal();
|
||||
|
||||
const { execute, isLoading } = useAction(stripeRedirect, {
|
||||
onSuccess: (data) => {
|
||||
window.location.href = data;
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error);
|
||||
},
|
||||
});
|
||||
|
||||
const onClick = () => {
|
||||
if (isPro) {
|
||||
execute({});
|
||||
} else {
|
||||
proModal.onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button disabled={isLoading} onClick={onClick} variant="primary">
|
||||
{isPro ? "Manage Subscription" : "Upgrade to Pro"}
|
||||
</Button>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
import { checkSubscription } from "@/lib/subscription";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
import { Info } from "../_components/info";
|
||||
import { SubscriptionButton } from "./_components/subscription-button";
|
||||
|
||||
const BillingPage = async () => {
|
||||
const isPro = await checkSubscription();
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Info isPro={isPro} />
|
||||
<Separator className="my-2" />
|
||||
<SubscriptionButton isPro={isPro} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BillingPage;
|
|
@ -0,0 +1,23 @@
|
|||
import { startCase } from "lodash";
|
||||
import { auth } from "@clerk/nextjs";
|
||||
|
||||
import { OrgControl } from "./_components/org-control";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const { orgSlug } = auth();
|
||||
|
||||
return {
|
||||
title: startCase(orgSlug ?? "organization"),
|
||||
};
|
||||
}
|
||||
|
||||
const OrganizationIdLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<>
|
||||
<OrgControl />
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationIdLayout;
|
|
@ -0,0 +1,25 @@
|
|||
import { Suspense } from "react";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { checkSubscription } from "@/lib/subscription";
|
||||
|
||||
import { Info } from "./_components/info";
|
||||
import { BoardList } from "./_components/board-list";
|
||||
|
||||
const OrganizationIdPage = async () => {
|
||||
const isPro = await checkSubscription();
|
||||
|
||||
return (
|
||||
<div className="w-full mb-20">
|
||||
<Info isPro={isPro} />
|
||||
<Separator className="my-4" />
|
||||
<div className="px-2 md:px-4">
|
||||
<Suspense fallback={<BoardList.Skeleton />}>
|
||||
<BoardList />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationIdPage;
|
|
@ -0,0 +1,25 @@
|
|||
import { OrganizationProfile } from "@clerk/nextjs";
|
||||
|
||||
const SettingsPage = () => {
|
||||
return (
|
||||
<div className="w-full">
|
||||
<OrganizationProfile
|
||||
appearance={{
|
||||
elements: {
|
||||
rootBox: {
|
||||
boxShadow: "none",
|
||||
width: "100%",
|
||||
},
|
||||
card: {
|
||||
border: "1px solid #e5e5e5",
|
||||
boxShadow: "none",
|
||||
width: "100%",
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsPage;
|
16
app/(platform)/(dashboard)/organization/layout.tsx
Normal file
16
app/(platform)/(dashboard)/organization/layout.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { Sidebar } from "../_components/sidebar";
|
||||
|
||||
const OrganizationLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<main className="pt-20 md:pt-24 px-4 max-w-6xl 2xl:max-w-screen-xl mx-auto">
|
||||
<div className="flex gap-x-7">
|
||||
<div className="w-64 shrink-0 hidden md:block">
|
||||
<Sidebar />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrganizationLayout;
|
Loading…
Add table
Add a link
Reference in a new issue