diff --git a/prisma/migrations/20241004154110_add_new_achievement/migration.sql b/prisma/migrations/20241004154110_add_new_achievement/migration.sql new file mode 100644 index 0000000..302b1e9 --- /dev/null +++ b/prisma/migrations/20241004154110_add_new_achievement/migration.sql @@ -0,0 +1,11 @@ +-- AlterEnum +-- This migration adds more than one value to an enum. +-- With PostgreSQL versions 11 and earlier, this is not possible +-- in a single migration. This can be worked around by creating +-- multiple migrations, each migration adding only one value to +-- the enum. + + +ALTER TYPE "AchievementType" ADD VALUE 'FiveCopyPastaADay'; +ALTER TYPE "AchievementType" ADD VALUE 'TagCollector'; +ALTER TYPE "AchievementType" ADD VALUE 'CollectionCurator'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 980ac34..4f3d6ef 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -201,6 +201,9 @@ enum AchievementType { OneMonthStreak ThreeMonthStreak SixMonthStreak + FiveCopyPastaADay + TagCollector + CollectionCurator } enum EngagementAction { diff --git a/src/app/(main)/collection/[id]/page.tsx b/src/app/(main)/collection/[id]/page.tsx index 3fedc70..4a4eb4e 100644 --- a/src/app/(main)/collection/[id]/page.tsx +++ b/src/app/(main)/collection/[id]/page.tsx @@ -3,7 +3,7 @@ import { api, HydrateClient } from "~/trpc/server"; import { Suspense } from "react"; import SkeletonCopyPasta from "~/components/Skeleton/CopyPasta"; import { type Metadata } from "next"; -import { trimContent } from "~/lib/utils"; +import { trimContent } from "~/utils"; import { notFound } from "next/navigation"; import { baseUrl } from "~/lib/constant"; import CollectionByIdPage from "~/app/_components/CollectionByIdPage"; diff --git a/src/app/(main)/copy-pasta/[id]/page.tsx b/src/app/(main)/copy-pasta/[id]/page.tsx index f9a901f..a770903 100644 --- a/src/app/(main)/copy-pasta/[id]/page.tsx +++ b/src/app/(main)/copy-pasta/[id]/page.tsx @@ -4,11 +4,7 @@ import CopyPastaPage from "~/app/_components/CopyPastaByIdPage"; import { Suspense } from "react"; import SkeletonCopyPasta from "~/components/Skeleton/CopyPasta"; import { type Metadata } from "next"; -import { - formatDateToHuman, - generateSchemaById, - trimContent, -} from "~/lib/utils"; +import { formatDateToHuman, generateSchemaById, trimContent } from "~/utils"; import { notFound } from "next/navigation"; import { baseUrl } from "~/lib/constant"; import { type Article } from "schema-dts"; diff --git a/src/app/(main)/user/[id]/page.tsx b/src/app/(main)/user/[id]/page.tsx index 8a63531..b11c152 100644 --- a/src/app/(main)/user/[id]/page.tsx +++ b/src/app/(main)/user/[id]/page.tsx @@ -1,7 +1,7 @@ import Layout from "~/components/Common/Layout"; import { api, HydrateClient } from "~/trpc/server"; import { type Metadata } from "next"; -import { trimContent } from "~/lib/utils"; +import { trimContent } from "~/utils"; import { notFound } from "next/navigation"; import { baseUrl } from "~/lib/constant"; import UserCopyPastaPage from "~/app/_components/UserCopyPastaPage"; diff --git a/src/app/_components/CollectionByIdPage.tsx b/src/app/_components/CollectionByIdPage.tsx index 24b4786..6dbbb27 100644 --- a/src/app/_components/CollectionByIdPage.tsx +++ b/src/app/_components/CollectionByIdPage.tsx @@ -6,7 +6,7 @@ import CardById from "~/components/Collection/CardById"; import CardCollectionDescription from "~/components/Collection/CardCollectionDescription"; import CardList from "~/components/Collection/CardLists"; import { type CardCopyPastaMinimal } from "~/lib/interface"; -import { getBreadcrumbs } from "~/lib/utils"; +import { getBreadcrumbs } from "~/utils"; import { api } from "~/trpc/react"; interface CollectionByIdProps { diff --git a/src/app/_components/CopyPastaByIdPage.tsx b/src/app/_components/CopyPastaByIdPage.tsx index 4211605..cf543fc 100644 --- a/src/app/_components/CopyPastaByIdPage.tsx +++ b/src/app/_components/CopyPastaByIdPage.tsx @@ -5,7 +5,7 @@ import BreadCrumbs from "~/components/Common/BreadCrumbs"; import CardById from "~/components/CopyPasta/CardById"; import CardRelated from "~/components/CopyPasta/CardRelated"; import SkeletonCopyPasta from "~/components/Skeleton/CopyPasta"; -import { getBreadcrumbs, getTweetId, trimContent } from "~/lib/utils"; +import { getBreadcrumbs, getTweetId, trimContent } from "~/utils"; import { api } from "~/trpc/react"; import TweetPage from "./TweetPage"; diff --git a/src/app/_components/CreateCollectionPage.tsx b/src/app/_components/CreateCollectionPage.tsx index 41e1f40..ddcc558 100644 --- a/src/app/_components/CreateCollectionPage.tsx +++ b/src/app/_components/CreateCollectionPage.tsx @@ -28,7 +28,7 @@ import { ScrollBar, ScrollArea } from "~/components/ui/scroll-area"; import EmptyState from "~/components/Common/EmptyState"; import CardList from "~/components/Collection/CardLists"; import BreadCrumbs from "~/components/Common/BreadCrumbs"; -import { getBreadcrumbs } from "~/lib/utils"; +import { getBreadcrumbs } from "~/utils"; export default function CreateCollection() { const createMutation = api.collection.create.useMutation(); diff --git a/src/app/_components/CreateCopyPastaPage.tsx b/src/app/_components/CreateCopyPastaPage.tsx index aabdfbb..833a1b0 100644 --- a/src/app/_components/CreateCopyPastaPage.tsx +++ b/src/app/_components/CreateCopyPastaPage.tsx @@ -30,7 +30,7 @@ import { formatDateToHuman, getBreadcrumbs, getTweetId, -} from "~/lib/utils"; +} from "~/utils"; import { createCopyPastaFormClient } from "../../server/form/copyPasta"; import { type z } from "zod"; import useToast from "~/components/ui/use-react-hot-toast"; diff --git a/src/app/_components/Dashboard/CollectionPage.tsx b/src/app/_components/Dashboard/CollectionPage.tsx index 09bcfa6..70dea6c 100644 --- a/src/app/_components/Dashboard/CollectionPage.tsx +++ b/src/app/_components/Dashboard/CollectionPage.tsx @@ -9,7 +9,7 @@ import EmptyState from "~/components/Common/EmptyState"; import GetContent from "~/components/Common/GetContent"; import { useSession } from "~/components/Common/SessionContext"; import { Button, buttonVariants } from "~/components/ui/button"; -import { cn, getBreadcrumbs } from "~/lib/utils"; +import { cn, getBreadcrumbs } from "~/utils"; import { api } from "~/trpc/react"; export default function CollectionPage() { diff --git a/src/app/_components/Dashboard/CopyPastaPage.tsx b/src/app/_components/Dashboard/CopyPastaPage.tsx index f9ac27f..8711d93 100644 --- a/src/app/_components/Dashboard/CopyPastaPage.tsx +++ b/src/app/_components/Dashboard/CopyPastaPage.tsx @@ -6,7 +6,7 @@ import BreadCrumbs from "~/components/Common/BreadCrumbs"; import DashboardCopyPastaAdmin from "~/components/Dashboard/CopyPasta/DashboardCopyPastaAdmin"; import DashboardCopyPastaUser from "~/components/Dashboard/CopyPasta/DashboardCopyPastaUser"; import { useSession } from "~/components/Common/SessionContext"; -import { getBreadcrumbs } from "~/lib/utils"; +import { getBreadcrumbs } from "~/utils"; export default function ProfileCopyPastaPage() { const session = useSession(); diff --git a/src/app/_components/Dashboard/EditCollectionPage.tsx b/src/app/_components/Dashboard/EditCollectionPage.tsx index 81a2253..8cfc2e7 100644 --- a/src/app/_components/Dashboard/EditCollectionPage.tsx +++ b/src/app/_components/Dashboard/EditCollectionPage.tsx @@ -28,7 +28,7 @@ import { ScrollBar, ScrollArea } from "~/components/ui/scroll-area"; import EmptyState from "~/components/Common/EmptyState"; import CardList from "~/components/Collection/CardLists"; import BreadCrumbs from "~/components/Common/BreadCrumbs"; -import { getBreadcrumbs } from "~/lib/utils"; +import { getBreadcrumbs } from "~/utils"; export default function EditCollectionPage({ id }: { id: string }) { const [collection] = api.collection.byId.useSuspenseQuery({ diff --git a/src/app/_components/Dashboard/ProfilePage.tsx b/src/app/_components/Dashboard/ProfilePage.tsx index 9589e51..ca4c20b 100644 --- a/src/app/_components/Dashboard/ProfilePage.tsx +++ b/src/app/_components/Dashboard/ProfilePage.tsx @@ -9,7 +9,7 @@ import { useSession } from "~/components/Common/SessionContext"; import { buttonVariants } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import UserProfileCard from "~/components/Common/UserProfileCard"; -import { cn, getBreadcrumbs } from "~/lib/utils"; +import { cn, getBreadcrumbs } from "~/utils"; export default function ProfilePage() { const session = useSession(); diff --git a/src/app/_components/EditCopyPastaPage.tsx b/src/app/_components/EditCopyPastaPage.tsx index 4abd999..290ddf2 100644 --- a/src/app/_components/EditCopyPastaPage.tsx +++ b/src/app/_components/EditCopyPastaPage.tsx @@ -15,7 +15,7 @@ import { Input } from "~/components/ui/input"; import { Textarea } from "~/components/ui/textarea"; import { api } from "~/trpc/react"; import { Pencil } from "lucide-react"; -import { determineSource, formatDateToHuman } from "~/lib/utils"; +import { determineSource, formatDateToHuman } from "~/utils"; import { editCopyPastaForm } from "../../server/form/copyPasta"; import { type z } from "zod"; import useToast from "~/components/ui/use-react-hot-toast"; diff --git a/src/app/_components/ListCollectionPage.tsx b/src/app/_components/ListCollectionPage.tsx index f80e75a..f1a1222 100644 --- a/src/app/_components/ListCollectionPage.tsx +++ b/src/app/_components/ListCollectionPage.tsx @@ -7,7 +7,7 @@ import GetContent from "~/components/Common/GetContent"; import { Button } from "~/components/ui/button"; import { ANALYTICS_EVENT } from "~/lib/constant"; import { trackEvent } from "~/lib/track"; -import { getBreadcrumbs } from "~/lib/utils"; +import { getBreadcrumbs } from "~/utils"; import { api } from "~/trpc/react"; export default function ListCollectionPage() { diff --git a/src/app/_components/ListCopyPastaPage.tsx b/src/app/_components/ListCopyPastaPage.tsx index b3502dd..892480d 100644 --- a/src/app/_components/ListCopyPastaPage.tsx +++ b/src/app/_components/ListCopyPastaPage.tsx @@ -6,7 +6,7 @@ import SkeletonListCopyPasta from "~/components/Skeleton/ListCopyPasta"; import { usePathname, useSearchParams } from "next/navigation"; import SkeletonTrending from "~/components/Skeleton/Trending"; import BreadCrumbs from "~/components/Common/BreadCrumbs"; -import { getBreadcrumbs } from "~/lib/utils"; +import { getBreadcrumbs } from "~/utils"; const TrendingHome = dynamic(() => import("~/components/Trending/Trending"), { ssr: false, diff --git a/src/app/_components/RankingPage.tsx b/src/app/_components/RankingPage.tsx index 562f727..6d99e93 100644 --- a/src/app/_components/RankingPage.tsx +++ b/src/app/_components/RankingPage.tsx @@ -9,7 +9,7 @@ import { TableBody, TableCell, } from "~/components/ui/table"; -import { getBreadcrumbs, getMedal } from "~/lib/utils"; +import { getBreadcrumbs, getMedal } from "~/utils"; import { api } from "~/trpc/react"; import { Accordion, diff --git a/src/app/_components/StatisticsPage.tsx b/src/app/_components/StatisticsPage.tsx index 7ad1018..95283c7 100644 --- a/src/app/_components/StatisticsPage.tsx +++ b/src/app/_components/StatisticsPage.tsx @@ -30,7 +30,7 @@ import { ChartTooltipContent, } from "~/components/ui/chart"; import { DAYS } from "~/lib/constant"; -import { formatDateToHuman, getBreadcrumbs, parseDate } from "~/lib/utils"; +import { formatDateToHuman, getBreadcrumbs, parseDate } from "~/utils"; import { api } from "~/trpc/react"; const chartConfigDonutReaction = { diff --git a/src/app/_components/SupportPage.tsx b/src/app/_components/SupportPage.tsx index c5cea68..cfb54f2 100644 --- a/src/app/_components/SupportPage.tsx +++ b/src/app/_components/SupportPage.tsx @@ -7,7 +7,7 @@ import { usePathname } from "next/navigation"; import BreadCrumbs from "~/components/Common/BreadCrumbs"; import { buttonVariants } from "~/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; -import { cn, getBreadcrumbs } from "~/lib/utils"; +import { cn, getBreadcrumbs } from "~/utils"; export default function SupportPage() { const pathname = usePathname(); diff --git a/src/app/_components/TweetPage.tsx b/src/app/_components/TweetPage.tsx index 5fcab59..3a7564d 100644 --- a/src/app/_components/TweetPage.tsx +++ b/src/app/_components/TweetPage.tsx @@ -6,7 +6,7 @@ import { type Tweet } from "react-tweet/api"; import EmptyState from "~/components/Common/EmptyState"; import CustomTweet from "~/components/Tweet/CustomTweet"; import SkeletonTweet from "~/components/Tweet/SkeletonTweet"; -import { sanitizeTweetEnrich } from "~/lib/utils"; +import { sanitizeTweetEnrich } from "~/utils"; interface TweetPageProps { id: string; diff --git a/src/app/page.tsx b/src/app/page.tsx index 6143883..44a4d02 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,7 +4,7 @@ import Hero from "~/components/Common/Hero"; import { type Metadata } from "next"; import { baseUrl } from "~/lib/constant"; import { Homepage } from "./_components/Homepage"; -import { generateSchemaOrgWebSite } from "~/lib/utils"; +import { generateSchemaOrgWebSite } from "~/utils"; type Props = { params: { id: string }; diff --git a/src/components/Collection/CardById.tsx b/src/components/Collection/CardById.tsx index ca6cdf4..db0bd35 100644 --- a/src/components/Collection/CardById.tsx +++ b/src/components/Collection/CardById.tsx @@ -13,7 +13,7 @@ import { NotebookPen, } from "lucide-react"; import { ANALYTICS_EVENT, sourceEnumHash } from "~/lib/constant"; -import { cn, trimContent } from "~/lib/utils"; +import { cn, trimContent } from "~/utils"; import { Button, buttonVariants } from "~/components/ui/button"; import Link from "next/link"; import Tag from "~/components/ui/tags"; diff --git a/src/components/Collection/CardCollectionDescription.tsx b/src/components/Collection/CardCollectionDescription.tsx index 36600f3..9220206 100644 --- a/src/components/Collection/CardCollectionDescription.tsx +++ b/src/components/Collection/CardCollectionDescription.tsx @@ -1,5 +1,5 @@ import { CalendarDays, Check, ChevronRight, Pencil, Trash } from "lucide-react"; -import { formatDateToHuman, cn } from "~/lib/utils"; +import { formatDateToHuman, cn } from "~/utils"; import Avatar from "~/components/ui/avatar-image"; import { Badge, badgeVariants } from "~/components/ui/badge"; import { diff --git a/src/components/Collection/CardLists.tsx b/src/components/Collection/CardLists.tsx index b888c50..5430b1c 100644 --- a/src/components/Collection/CardLists.tsx +++ b/src/components/Collection/CardLists.tsx @@ -1,4 +1,4 @@ -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; interface CollectionListProps { listOfCollections: T[]; diff --git a/src/components/Collection/CardSearchResult.tsx b/src/components/Collection/CardSearchResult.tsx index b15bcc1..789bd2a 100644 --- a/src/components/Collection/CardSearchResult.tsx +++ b/src/components/Collection/CardSearchResult.tsx @@ -17,7 +17,7 @@ import { Plus, } from "lucide-react"; import { ANALYTICS_EVENT } from "~/lib/constant"; -import { cn, trimContent } from "~/lib/utils"; +import { cn, trimContent } from "~/utils"; import { Button, buttonVariants } from "~/components/ui/button"; import Link from "next/link"; import Tag from "~/components/ui/tags"; diff --git a/src/components/Common/EmptyState.tsx b/src/components/Common/EmptyState.tsx index f0adcd8..3133b1c 100644 --- a/src/components/Common/EmptyState.tsx +++ b/src/components/Common/EmptyState.tsx @@ -1,5 +1,5 @@ import { type ReactElement, type HTMLAttributes } from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; interface EmptyStateProps extends HTMLAttributes { message?: string | ReactElement; diff --git a/src/components/Common/Hero.tsx b/src/components/Common/Hero.tsx index c22a75c..f4cd693 100644 --- a/src/components/Common/Hero.tsx +++ b/src/components/Common/Hero.tsx @@ -4,7 +4,7 @@ import { NotebookPen, Search } from "lucide-react"; import React from "react"; import { buttonVariants } from "~/components/ui/button"; import Link from "next/link"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import Marquee from "~/components/magicui/marquee"; import DotPattern from "~/components/magicui/dot-pattern"; import CardDisplay from "~/components/CopyPasta/CardDisplay"; diff --git a/src/components/Common/SearchBar.tsx b/src/components/Common/SearchBar.tsx index aa07552..b97c9c8 100644 --- a/src/components/Common/SearchBar.tsx +++ b/src/components/Common/SearchBar.tsx @@ -4,7 +4,7 @@ import { LoaderCircle, Search } from "lucide-react"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; import Link from "next/link"; -import { trimContent } from "~/lib/utils"; +import { trimContent } from "~/utils"; import { api } from "~/trpc/react"; import { useMediaQuery, useDebounce } from "@uidotdev/usehooks"; import { ANALYTICS_EVENT } from "~/lib/constant"; diff --git a/src/components/Common/UserProfileCard.tsx b/src/components/Common/UserProfileCard.tsx index 3c1420d..6b568db 100644 --- a/src/components/Common/UserProfileCard.tsx +++ b/src/components/Common/UserProfileCard.tsx @@ -18,7 +18,7 @@ import { import { Input } from "~/components/ui/input"; import useToast from "~/components/ui/use-react-hot-toast"; import { api } from "~/trpc/react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import ReactionSummaryProfile from "~/components/Reaction/ReactionSummaryProfile"; import { v4 as uuidv4 } from "uuid"; import { editProfile } from "~/server/form/user"; diff --git a/src/components/CopyPasta/CardById.tsx b/src/components/CopyPasta/CardById.tsx index d2f007c..577cd57 100644 --- a/src/components/CopyPasta/CardById.tsx +++ b/src/components/CopyPasta/CardById.tsx @@ -7,7 +7,7 @@ import { } from "~/components/ui/card"; import Link from "next/link"; import { type Tag as TagType } from "@prisma/client"; -import { cn, formatDateToHuman, trimContent } from "~/lib/utils"; +import { cn, formatDateToHuman, trimContent } from "~/utils"; import { buttonVariants } from "~/components/ui/button"; import { BookCheck, diff --git a/src/components/CopyPasta/CardDashboard.tsx b/src/components/CopyPasta/CardDashboard.tsx index 8c204d1..46b3ef5 100644 --- a/src/components/CopyPasta/CardDashboard.tsx +++ b/src/components/CopyPasta/CardDashboard.tsx @@ -8,7 +8,7 @@ import { } from "~/components/ui/card"; import { ArrowRight, Link as LinkIcon, NotebookPen } from "lucide-react"; import { ANALYTICS_EVENT, sourceEnumHash } from "~/lib/constant"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import { buttonVariants } from "~/components/ui/button"; import Link from "next/link"; import Tag from "~/components/ui/tags"; diff --git a/src/components/CopyPasta/CardDisplay.tsx b/src/components/CopyPasta/CardDisplay.tsx index d52ce8b..a1bfa9b 100644 --- a/src/components/CopyPasta/CardDisplay.tsx +++ b/src/components/CopyPasta/CardDisplay.tsx @@ -1,4 +1,4 @@ -import { cn, trimContent } from "~/lib/utils"; +import { cn, trimContent } from "~/utils"; import { Card, CardContent, CardFooter } from "~/components/ui/card"; import Tag from "~/components/ui/tags"; import Link from "next/link"; diff --git a/src/components/CopyPasta/CardMinimal.tsx b/src/components/CopyPasta/CardMinimal.tsx index 1a8bf43..2e082f3 100644 --- a/src/components/CopyPasta/CardMinimal.tsx +++ b/src/components/CopyPasta/CardMinimal.tsx @@ -13,7 +13,7 @@ import { NotebookPen, } from "lucide-react"; import { ANALYTICS_EVENT, sourceEnumHash } from "~/lib/constant"; -import { cn, trimContent } from "~/lib/utils"; +import { cn, trimContent } from "~/utils"; import ReactionSummary from "../Reaction/ReactionSummary"; import { Button, buttonVariants } from "~/components/ui/button"; import Link from "next/link"; diff --git a/src/components/CopyPasta/CardRelated.tsx b/src/components/CopyPasta/CardRelated.tsx index b744e43..3de01a0 100644 --- a/src/components/CopyPasta/CardRelated.tsx +++ b/src/components/CopyPasta/CardRelated.tsx @@ -13,7 +13,7 @@ import { NotebookPen, } from "lucide-react"; import { ANALYTICS_EVENT, sourceEnumHash } from "~/lib/constant"; -import { cn, trimContent } from "~/lib/utils"; +import { cn, trimContent } from "~/utils"; import { buttonVariants } from "~/components/ui/button"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; diff --git a/src/components/CopyPasta/DialogImage.tsx b/src/components/CopyPasta/DialogImage.tsx index ab789aa..6cd07d9 100644 --- a/src/components/CopyPasta/DialogImage.tsx +++ b/src/components/CopyPasta/DialogImage.tsx @@ -8,7 +8,7 @@ import { DialogTrigger, } from "~/components/ui/dialog"; import { Button } from "~/components/ui/button"; -import { trimContent } from "~/lib/utils"; +import { trimContent } from "~/utils"; import Image from "next/image"; import { useState, useEffect } from "react"; diff --git a/src/components/Dashboard/EngagementLogHistory.tsx b/src/components/Dashboard/EngagementLogHistory.tsx index 278b552..644bea3 100644 --- a/src/components/Dashboard/EngagementLogHistory.tsx +++ b/src/components/Dashboard/EngagementLogHistory.tsx @@ -1,6 +1,6 @@ "use client"; -import { formatDateDistance, parseEngagementLogs } from "~/lib/utils"; +import { formatDateDistance, parseEngagementLogs } from "~/utils"; import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card"; import { api } from "~/trpc/react"; import { type ActionType, type EngagementActionDataDb } from "~/lib/interface"; diff --git a/src/components/Reaction/ReactionSummary.tsx b/src/components/Reaction/ReactionSummary.tsx index 0f44f4a..b36a4c4 100644 --- a/src/components/Reaction/ReactionSummary.tsx +++ b/src/components/Reaction/ReactionSummary.tsx @@ -2,7 +2,7 @@ import { type $Enums, EmotionType } from "@prisma/client"; import dynamic from "next/dynamic"; import { Badge } from "~/components/ui/badge"; import { Skeleton } from "~/components/ui/skeleton"; -import { mergeReactions } from "~/lib/utils"; +import { mergeReactions } from "~/utils"; const ReactionSummaryChild = dynamic(() => import("./ReactionSummaryChild"), { ssr: false, diff --git a/src/components/Reaction/ReactionSummaryChild.tsx b/src/components/Reaction/ReactionSummaryChild.tsx index 6eaa2a6..ebad92e 100644 --- a/src/components/Reaction/ReactionSummaryChild.tsx +++ b/src/components/Reaction/ReactionSummaryChild.tsx @@ -8,7 +8,7 @@ import { api } from "~/trpc/react"; import { type EmotionType } from "@prisma/client"; import { session } from "../Common/HOCSession"; import { useEffect, useState } from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import { trackEvent } from "~/lib/track"; import Lottie from "react-lottie-player"; diff --git a/src/components/Skeleton/CopyPasta.tsx b/src/components/Skeleton/CopyPasta.tsx index 020e817..3fe27ee 100644 --- a/src/components/Skeleton/CopyPasta.tsx +++ b/src/components/Skeleton/CopyPasta.tsx @@ -1,4 +1,4 @@ -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import { Card, CardContent } from "~/components/ui/card"; import { Skeleton } from "~/components/ui/skeleton"; diff --git a/src/components/Trending/Trending.tsx b/src/components/Trending/Trending.tsx index a436891..a33fcee 100644 --- a/src/components/Trending/Trending.tsx +++ b/src/components/Trending/Trending.tsx @@ -3,7 +3,7 @@ import { useMediaQuery } from "@uidotdev/usehooks"; import { ArrowRight, Eye, Hash, Library, TrendingUp } from "lucide-react"; import Link from "next/link"; -import { cn, trimContent } from "~/lib/utils"; +import { cn, trimContent } from "~/utils"; import { api } from "~/trpc/react"; import { ScrollArea } from "~/components/ui/scroll-area"; import ListTags from "~/components/Common/ListTags"; diff --git a/src/components/Tweet/CustomTweet.tsx b/src/components/Tweet/CustomTweet.tsx index 028a961..824dd3b 100644 --- a/src/components/Tweet/CustomTweet.tsx +++ b/src/components/Tweet/CustomTweet.tsx @@ -10,7 +10,7 @@ import { Heart, Link2, MessageCircle, Repeat2 } from "lucide-react"; import Link from "next/link"; import { AvatarImage, AvatarFallback, Avatar } from "~/components/ui/avatar"; import { parseISO } from "date-fns"; -import { formatDateToHuman, cn, sanitizeTweet } from "~/lib/utils"; +import { formatDateToHuman, cn, sanitizeTweet } from "~/utils"; import { buttonVariants } from "~/components/ui/button"; import QuotedTweet from "./QuotedTweet"; import CustomTweetBody from "./CustomTweetBody"; diff --git a/src/components/Tweet/QuotedTweet.tsx b/src/components/Tweet/QuotedTweet.tsx index 85ccf3a..92eb482 100644 --- a/src/components/Tweet/QuotedTweet.tsx +++ b/src/components/Tweet/QuotedTweet.tsx @@ -9,7 +9,7 @@ import { type QuotedTweet } from "react-tweet/api"; import { Heart, MessageCircle, Repeat2 } from "lucide-react"; import { AvatarImage, AvatarFallback, Avatar } from "~/components/ui/avatar"; import { parseISO } from "date-fns"; -import { formatDateToHuman } from "~/lib/utils"; +import { formatDateToHuman } from "~/utils"; type CustomTweetProps = { tweet: QuotedTweet; diff --git a/src/components/magicui/animated-gradient-text.tsx b/src/components/magicui/animated-gradient-text.tsx index 7ba8702..de27835 100644 --- a/src/components/magicui/animated-gradient-text.tsx +++ b/src/components/magicui/animated-gradient-text.tsx @@ -1,6 +1,6 @@ import { type ReactNode } from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; export default function AnimatedGradientText({ children, @@ -17,7 +17,7 @@ export default function AnimatedGradientText({ )} >
{children} diff --git a/src/components/magicui/blur-in.tsx b/src/components/magicui/blur-in.tsx index 7594835..1f35cf8 100644 --- a/src/components/magicui/blur-in.tsx +++ b/src/components/magicui/blur-in.tsx @@ -2,7 +2,7 @@ import { motion } from "framer-motion"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; interface BlurIntProps { word: string; diff --git a/src/components/magicui/dot-pattern.tsx b/src/components/magicui/dot-pattern.tsx index 9d7c054..6bbd552 100644 --- a/src/components/magicui/dot-pattern.tsx +++ b/src/components/magicui/dot-pattern.tsx @@ -1,6 +1,6 @@ import { useId } from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; interface DotPatternProps { width?: any; diff --git a/src/components/magicui/marquee.tsx b/src/components/magicui/marquee.tsx index 419f665..30576f8 100644 --- a/src/components/magicui/marquee.tsx +++ b/src/components/magicui/marquee.tsx @@ -1,4 +1,4 @@ -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; interface MarqueeProps { className?: string; diff --git a/src/components/magicui/number-ticker.tsx b/src/components/magicui/number-ticker.tsx index f1a8f2f..c629ba9 100644 --- a/src/components/magicui/number-ticker.tsx +++ b/src/components/magicui/number-ticker.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef } from "react"; import { useInView, useMotionValue, useSpring } from "framer-motion"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; export default function NumberTicker({ value, @@ -46,7 +46,7 @@ export default function NumberTicker({ return ( ; diff --git a/src/components/magicui/rainbow-button.tsx b/src/components/magicui/rainbow-button.tsx index 5dc0735..9d2a9e1 100644 --- a/src/components/magicui/rainbow-button.tsx +++ b/src/components/magicui/rainbow-button.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; type RainbowButtonProps = React.ButtonHTMLAttributes; diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index 4bd88ac..c5e2f9c 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import * as AccordionPrimitive from "@radix-ui/react-accordion"; import { ChevronDown } from "lucide-react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; const Accordion = AccordionPrimitive.Root; diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 05e961c..5ae086f 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as AvatarPrimitive from "@radix-ui/react-avatar" +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; const Avatar = React.forwardRef< React.ElementRef, @@ -13,12 +13,12 @@ const Avatar = React.forwardRef< ref={ref} className={cn( "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className + className, )} {...props} /> -)) -Avatar.displayName = AvatarPrimitive.Root.displayName +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; const AvatarImage = React.forwardRef< React.ElementRef, @@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef< className={cn("aspect-square h-full w-full", className)} {...props} /> -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; const AvatarFallback = React.forwardRef< React.ElementRef, @@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef< ref={ref} className={cn( "flex h-full w-full items-center justify-center rounded-full bg-muted", - className + className, )} {...props} /> -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; -export { Avatar, AvatarImage, AvatarFallback } +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index 2a69fb0..611b3f1 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; const badgeVariants = cva( "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 cursor-pointer", diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx index 03c2f9e..5ccda83 100644 --- a/src/components/ui/breadcrumb.tsx +++ b/src/components/ui/breadcrumb.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { Slot } from "@radix-ui/react-slot"; import { ChevronRight, MoreHorizontal } from "lucide-react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; const Breadcrumb = React.forwardRef< HTMLElement, diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 86822f9..452f754 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -2,7 +2,7 @@ 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"; +import { cn } from "~/utils"; const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", diff --git a/src/components/ui/calendar.tsx b/src/components/ui/calendar.tsx index 5387aa6..5c3caf8 100644 --- a/src/components/ui/calendar.tsx +++ b/src/components/ui/calendar.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import { ChevronLeft, ChevronRight } from "lucide-react"; import { DayPicker } from "react-day-picker"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import { buttonVariants } from "~/components/ui/button"; export type CalendarProps = React.ComponentProps; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 3d03b39..ca777c8 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; const Card = React.forwardRef< HTMLDivElement, diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index 362f392..c1ba133 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import * as RechartsPrimitive from "recharts"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; // Format: { THEME_NAME: CSS_SELECTOR } const THEMES = { light: "", dark: ".dark" } as const; diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index ad4ddbc..a1218c0 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -1,10 +1,10 @@ -"use client" +"use client"; -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { Check } from "lucide-react" +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; const Checkbox = React.forwardRef< React.ElementRef, @@ -14,7 +14,7 @@ const Checkbox = React.forwardRef< ref={ref} className={cn( "peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", - className + className, )} {...props} > @@ -24,7 +24,7 @@ const Checkbox = React.forwardRef< -)) -Checkbox.displayName = CheckboxPrimitive.Root.displayName +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; -export { Checkbox } +export { Checkbox }; diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx index b1ab842..27533ab 100644 --- a/src/components/ui/command.tsx +++ b/src/components/ui/command.tsx @@ -5,7 +5,7 @@ import { type DialogProps } from "@radix-ui/react-dialog"; import { Command as CommandPrimitive } from "cmdk"; import { Search } from "lucide-react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import { Dialog, DialogContent } from "~/components/ui/dialog"; const Command = React.forwardRef< diff --git a/src/components/ui/datetime-picker.tsx b/src/components/ui/datetime-picker.tsx index 297d313..4707aca 100644 --- a/src/components/ui/datetime-picker.tsx +++ b/src/components/ui/datetime-picker.tsx @@ -7,7 +7,7 @@ import { ChevronLeft, ChevronRight, } from "lucide-react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import { Input } from "~/components/ui/input"; import { Button, buttonVariants } from "~/components/ui/button"; import { type CalendarProps } from "~/components/ui/calendar"; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 9850daa..11c5552 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -1,18 +1,18 @@ -"use client" +"use client"; -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; -const Dialog = DialogPrimitive.Root +const Dialog = DialogPrimitive.Root; -const DialogTrigger = DialogPrimitive.Trigger +const DialogTrigger = DialogPrimitive.Trigger; -const DialogPortal = DialogPrimitive.Portal +const DialogPortal = DialogPrimitive.Portal; -const DialogClose = DialogPrimitive.Close +const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< React.ElementRef, @@ -21,13 +21,13 @@ const DialogOverlay = React.forwardRef< -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< React.ElementRef, @@ -39,7 +39,7 @@ const DialogContent = React.forwardRef< ref={ref} className={cn( "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", - className + className, )} {...props} > @@ -50,8 +50,8 @@ const DialogContent = React.forwardRef< -)) -DialogContent.displayName = DialogPrimitive.Content.displayName +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; const DialogHeader = ({ className, @@ -60,12 +60,12 @@ const DialogHeader = ({
-) -DialogHeader.displayName = "DialogHeader" +); +DialogHeader.displayName = "DialogHeader"; const DialogFooter = ({ className, @@ -74,12 +74,12 @@ const DialogFooter = ({
-) -DialogFooter.displayName = "DialogFooter" +); +DialogFooter.displayName = "DialogFooter"; const DialogTitle = React.forwardRef< React.ElementRef, @@ -89,12 +89,12 @@ const DialogTitle = React.forwardRef< ref={ref} className={cn( "text-lg font-semibold leading-none tracking-tight", - className + className, )} {...props} /> -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< React.ElementRef, @@ -105,8 +105,8 @@ const DialogDescription = React.forwardRef< className={cn("text-sm text-muted-foreground", className)} {...props} /> -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; export { Dialog, @@ -119,4 +119,4 @@ export { DialogFooter, DialogTitle, DialogDescription, -} +}; diff --git a/src/components/ui/dropdown-menu.tsx b/src/components/ui/dropdown-menu.tsx index a4b85a3..c7290bb 100644 --- a/src/components/ui/dropdown-menu.tsx +++ b/src/components/ui/dropdown-menu.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; import { Check, ChevronRight, Circle } from "lucide-react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; const DropdownMenu = DropdownMenuPrimitive.Root; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index 09f6f76..e715a06 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -12,7 +12,7 @@ import { useFormContext, } from "react-hook-form"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import { Label } from "~/components/ui/label"; const Form = FormProvider; diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx index 38555a4..098e721 100644 --- a/src/components/ui/hover-card.tsx +++ b/src/components/ui/hover-card.tsx @@ -1,15 +1,15 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -"use client" +"use client"; -import * as React from "react" -import * as HoverCardPrimitive from "@radix-ui/react-hover-card" +import * as React from "react"; +import * as HoverCardPrimitive from "@radix-ui/react-hover-card"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; -const HoverCard = HoverCardPrimitive.Root +const HoverCard = HoverCardPrimitive.Root; -const HoverCardTrigger = HoverCardPrimitive.Trigger +const HoverCardTrigger = HoverCardPrimitive.Trigger; const HoverCardContent = React.forwardRef< React.ElementRef, @@ -21,11 +21,11 @@ const HoverCardContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> -)) -HoverCardContent.displayName = HoverCardPrimitive.Content.displayName +)); +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName; -export { HoverCard, HoverCardTrigger, HoverCardContent } +export { HoverCard, HoverCardTrigger, HoverCardContent }; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 31e5142..1a69ab9 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; export type InputProps = React.InputHTMLAttributes; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx index 8f40738..a0eb261 100644 --- a/src/components/ui/label.tsx +++ b/src/components/ui/label.tsx @@ -1,14 +1,14 @@ -"use client" +"use client"; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" -) + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", +); const Label = React.forwardRef< React.ElementRef, @@ -20,7 +20,7 @@ const Label = React.forwardRef< className={cn(labelVariants(), className)} {...props} /> -)) -Label.displayName = LabelPrimitive.Root.displayName +)); +Label.displayName = LabelPrimitive.Root.displayName; -export { Label } +export { Label }; diff --git a/src/components/ui/multiple-selector.tsx b/src/components/ui/multiple-selector.tsx index 8b0d331..9fb15a7 100644 --- a/src/components/ui/multiple-selector.tsx +++ b/src/components/ui/multiple-selector.tsx @@ -12,7 +12,7 @@ import { CommandItem, CommandList, } from "~/components/ui/command"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; export interface Option { value: string; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index d45a633..e9fd598 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -1,13 +1,13 @@ -"use client" +"use client"; -import * as React from "react" -import * as PopoverPrimitive from "@radix-ui/react-popover" +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; -const Popover = PopoverPrimitive.Root +const Popover = PopoverPrimitive.Root; -const PopoverTrigger = PopoverPrimitive.Trigger +const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverContent = React.forwardRef< React.ElementRef, @@ -20,12 +20,12 @@ const PopoverContent = React.forwardRef< sideOffset={sideOffset} className={cn( "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className + className, )} {...props} /> -)) -PopoverContent.displayName = PopoverPrimitive.Content.displayName +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; -export { Popover, PopoverTrigger, PopoverContent } +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx index d6991dd..d84f168 100644 --- a/src/components/ui/radio-group.tsx +++ b/src/components/ui/radio-group.tsx @@ -4,7 +4,7 @@ import * as React from "react"; import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"; import { Circle } from "lucide-react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; const RadioGroup = React.forwardRef< React.ElementRef, diff --git a/src/components/ui/scroll-area.tsx b/src/components/ui/scroll-area.tsx index f66ab4f..fe7a3ee 100644 --- a/src/components/ui/scroll-area.tsx +++ b/src/components/ui/scroll-area.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area" +import * as React from "react"; +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; const ScrollArea = React.forwardRef< React.ElementRef, @@ -20,8 +20,8 @@ const ScrollArea = React.forwardRef< -)) -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName +)); +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; const ScrollBar = React.forwardRef< React.ElementRef, @@ -36,13 +36,13 @@ const ScrollBar = React.forwardRef< "h-full w-2.5 border-l border-l-transparent p-[1px]", orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]", - className + className, )} {...props} > -)) -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName +)); +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; -export { ScrollArea, ScrollBar } +export { ScrollArea, ScrollBar }; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index 1091153..a808dfd 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -1,16 +1,16 @@ -"use client" +"use client"; -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { Check, ChevronDown, ChevronUp } from "lucide-react" +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; -const Select = SelectPrimitive.Root +const Select = SelectPrimitive.Root; -const SelectGroup = SelectPrimitive.Group +const SelectGroup = SelectPrimitive.Group; -const SelectValue = SelectPrimitive.Value +const SelectValue = SelectPrimitive.Value; const SelectTrigger = React.forwardRef< React.ElementRef, @@ -20,7 +20,7 @@ const SelectTrigger = React.forwardRef< ref={ref} className={cn( "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", - className + className, )} {...props} > @@ -29,8 +29,8 @@ const SelectTrigger = React.forwardRef< -)) -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; const SelectScrollUpButton = React.forwardRef< React.ElementRef, @@ -40,14 +40,14 @@ const SelectScrollUpButton = React.forwardRef< ref={ref} className={cn( "flex cursor-default items-center justify-center py-1", - className + className, )} {...props} > -)) -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; const SelectScrollDownButton = React.forwardRef< React.ElementRef, @@ -57,15 +57,15 @@ const SelectScrollDownButton = React.forwardRef< ref={ref} className={cn( "flex cursor-default items-center justify-center py-1", - className + className, )} {...props} > -)) +)); SelectScrollDownButton.displayName = - SelectPrimitive.ScrollDownButton.displayName + SelectPrimitive.ScrollDownButton.displayName; const SelectContent = React.forwardRef< React.ElementRef, @@ -78,7 +78,7 @@ const SelectContent = React.forwardRef< "relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", - className + className, )} position={position} {...props} @@ -88,7 +88,7 @@ const SelectContent = React.forwardRef< className={cn( "p-1", position === "popper" && - "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]" + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]", )} > {children} @@ -96,8 +96,8 @@ const SelectContent = React.forwardRef< -)) -SelectContent.displayName = SelectPrimitive.Content.displayName +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; const SelectLabel = React.forwardRef< React.ElementRef, @@ -108,8 +108,8 @@ const SelectLabel = React.forwardRef< className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} /> -)) -SelectLabel.displayName = SelectPrimitive.Label.displayName +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; const SelectItem = React.forwardRef< React.ElementRef, @@ -119,7 +119,7 @@ const SelectItem = React.forwardRef< ref={ref} className={cn( "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className + className, )} {...props} > @@ -131,8 +131,8 @@ const SelectItem = React.forwardRef< {children} -)) -SelectItem.displayName = SelectPrimitive.Item.displayName +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; const SelectSeparator = React.forwardRef< React.ElementRef, @@ -143,8 +143,8 @@ const SelectSeparator = React.forwardRef< className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} /> -)) -SelectSeparator.displayName = SelectPrimitive.Separator.displayName +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; export { Select, @@ -157,4 +157,4 @@ export { SelectSeparator, SelectScrollUpButton, SelectScrollDownButton, -} +}; diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx index 49d5182..15a9867 100644 --- a/src/components/ui/separator.tsx +++ b/src/components/ui/separator.tsx @@ -1,9 +1,9 @@ -"use client" +"use client"; -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; const Separator = React.forwardRef< React.ElementRef, @@ -11,7 +11,7 @@ const Separator = React.forwardRef< >( ( { className, orientation = "horizontal", decorative = true, ...props }, - ref + ref, ) => ( - ) -) -Separator.displayName = SeparatorPrimitive.Root.displayName + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; -export { Separator } +export { Separator }; diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx index 45ff9f7..335861c 100644 --- a/src/components/ui/skeleton.tsx +++ b/src/components/ui/skeleton.tsx @@ -1,4 +1,4 @@ -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; function Skeleton({ className, diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index 6491e51..e01a792 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; const Table = React.forwardRef< HTMLTableElement, diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 897452b..be8eee8 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -3,7 +3,7 @@ import * as React from "react"; import * as TabsPrimitive from "@radix-ui/react-tabs"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; const Tabs = TabsPrimitive.Root; diff --git a/src/components/ui/tags.tsx b/src/components/ui/tags.tsx index b244f03..205b02c 100644 --- a/src/components/ui/tags.tsx +++ b/src/components/ui/tags.tsx @@ -1,4 +1,4 @@ -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; import { Badge } from "./badge"; import { type Tag as TagType } from "@prisma/client"; import { Hash } from "lucide-react"; diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index 2a73386..f5df876 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -1,6 +1,6 @@ import * as React from "react"; -import { cn } from "~/lib/utils"; +import { cn } from "~/utils"; export type TextareaProps = React.TextareaHTMLAttributes; diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index b4bf1ad..a1a706e 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -1,13 +1,13 @@ -"use client" +"use client"; -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" +import * as React from "react"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; -import { cn } from "~/lib/utils" +import { cn } from "~/utils"; -const ToastProvider = ToastPrimitives.Provider +const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef< React.ElementRef, @@ -17,12 +17,12 @@ const ToastViewport = React.forwardRef< ref={ref} className={cn( "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", - className + className, )} {...props} /> -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", @@ -37,8 +37,8 @@ const toastVariants = cva( defaultVariants: { variant: "default", }, - } -) + }, +); const Toast = React.forwardRef< React.ElementRef, @@ -51,9 +51,9 @@ const Toast = React.forwardRef< className={cn(toastVariants({ variant }), className)} {...props} /> - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; const ToastAction = React.forwardRef< React.ElementRef, @@ -63,12 +63,12 @@ const ToastAction = React.forwardRef< ref={ref} className={cn( "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive", - className + className, )} {...props} /> -)) -ToastAction.displayName = ToastPrimitives.Action.displayName +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; const ToastClose = React.forwardRef< React.ElementRef, @@ -78,15 +78,15 @@ const ToastClose = React.forwardRef< ref={ref} className={cn( "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", - className + className, )} toast-close="" {...props} > -)) -ToastClose.displayName = ToastPrimitives.Close.displayName +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef< React.ElementRef, @@ -97,8 +97,8 @@ const ToastTitle = React.forwardRef< className={cn("text-sm font-semibold", className)} {...props} /> -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef< React.ElementRef, @@ -109,12 +109,12 @@ const ToastDescription = React.forwardRef< className={cn("text-sm opacity-90", className)} {...props} /> -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; -type ToastProps = React.ComponentPropsWithoutRef +type ToastProps = React.ComponentPropsWithoutRef; -type ToastActionElement = React.ReactElement +type ToastActionElement = React.ReactElement; export { type ToastProps, @@ -126,4 +126,4 @@ export { ToastDescription, ToastClose, ToastAction, -} +}; diff --git a/src/lib/constant.tsx b/src/lib/constant.tsx index f907600..eb3b486 100644 --- a/src/lib/constant.tsx +++ b/src/lib/constant.tsx @@ -161,3 +161,11 @@ export const ENGAGEMENT_SCORE = { }; export const TIMEZONE = "Asia/Jakarta"; + +export const ACHIEVEMENT_MINIMUM = { + TAG_COLLECTOR_MINIMUM: 15, + COLLECTION_CURATOR: { + COPY_PASTA_PER_COLLECTION: 5, + COLLECTION_MINIMUM: 3, + }, +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts deleted file mode 100644 index 616d13b..0000000 --- a/src/lib/utils.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -import { twMerge } from "tailwind-merge"; -import { type $Enums, EngagementAction, OriginSource } from "@prisma/client"; -import { type ClassValue, clsx } from "clsx"; -import { format, formatDistance, parse } from "date-fns"; -import { format as formatTz, toZonedTime } from "date-fns-tz"; -import { id } from "date-fns/locale"; -import { - type Breadcrumb, - type EngagementActionRecord, - type EngagementActionDataDb, -} from "./interface"; -import { - type Thing, - type WithContext, - type WebSite, - type SearchAction, -} from "schema-dts"; -import { baseUrl, TIMEZONE } from "./constant"; -import he from "he"; -import { type EnrichedTweet } from "react-tweet"; - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)); -} - -export function formatDateToHuman(date: Date, formatString = "PPP") { - return format(date, formatString, { locale: id }); -} - -export function formatDateDistance(date: Date) { - return formatDistance(date, new Date(), { locale: id, addSuffix: true }); -} - -export function parseDate(date: string) { - return parse(date, "d MMMM yyyy", new Date(), { locale: id }); -} - -export function getJakartaDate(date: Date = new Date()): Date { - return toZonedTime(date, TIMEZONE); -} - -export function getJakartaDateString(date: Date = new Date()): string { - return formatTz(getJakartaDate(date), "yyyy-MM-dd", { timeZone: TIMEZONE }); -} - -export function sanitizeTweet(text: string) { - return he.decode(text); -} - -export function sanitizeTweetEnrich(tweet: EnrichedTweet) { - return tweet.entities - .map((e) => { - switch (e.type) { - case "media": - case "symbol": - case "url": - return; - case "hashtag": - case "mention": - case "text": - return e.text; - } - }) - .join(" "); -} - -export function trimContent(content: string, length = 255) { - if (!content) { - return "😱😱😱"; - } - - const trimmedContent = content.slice(0, length).trimEnd(); - return trimmedContent + (content.length > length ? "..." : ""); -} - -export function getTweetId(tweetUrl: string) { - const match = tweetUrl.match(/\/status\/(\d+)/); - return match ? match[1] : undefined; -} - -export function getRandomElement(array: string[]) { - if (array.length === 0) { - throw new Error("Array cannot be empty"); - } - return array[Math.floor(Math.random() * array.length)]; -} - -export function determineSource(url = "Other") { - const regex = - /((https?:\/\/)?(?:www\.)?(facebook\.com|fb\.me|twitter\.com|t\.co|x\.com)\/[^\s]+)/; - if (url === "Other") { - return OriginSource.Other; - } - const match = RegExp(regex).exec(url.toLowerCase()); - if (!match) { - return OriginSource.Other; - } - const source = match[3]; - - if (source === "facebook.com" || source === "fb.me") { - return OriginSource.Facebook; - } else if ( - source === "twitter.com" || - source === "x.com" || - source === "t.co" - ) { - return OriginSource.Twitter; - } else { - return OriginSource.Other; - } -} - -export function getMedal(number: number) { - switch (number) { - case 1: - return "🥇 "; - case 2: - return "🥈 "; - case 3: - return "🥉 "; - default: - return number; - } -} - -export function mergeReactions( - data?: { - copyPastaId: string; - userId: string; - emotion: $Enums.EmotionType; - _count: { - emotion: number; - }; - }[], -) { - return data?.reduce( - (acc, curr) => { - const key = `${curr.emotion}`; - - if (!acc[key]) { - acc[key] = { - ...curr, - userIds: [], - _count: { emotion: 0 }, - }; - } - - acc[key]._count.emotion += curr._count.emotion; - acc[key].userIds.push(curr.userId); - - return acc; - }, - {} as Record< - string, - { - copyPastaId: string; - userIds: string[]; - emotion: $Enums.EmotionType; - _count: { - emotion: number; - }; - } - >, - ); -} - -export function getBreadcrumbs(url: string): Breadcrumb[] { - const parts = url.split("/").filter((part) => part !== ""); - const breadcrumbs: Breadcrumb[] = []; - let currentPath = ""; - - parts.forEach((part) => { - currentPath += `/${part}`; - const text = part; // Use the part itself as the text - breadcrumbs.push({ url: currentPath, text }); - }); - - return breadcrumbs; -} - -export function generateSchemaById(json: WithContext) { - return json; -} - -type QueryAction = SearchAction & { - "query-input": string; -}; - -export function generateSchemaOrgWebSite(): WithContext { - return { - "@context": "https://schema.org", - "@type": "WebSite", - name: "Arsip Template", - url: baseUrl, - potentialAction: { - "@type": "SearchAction", - target: { - "@type": "EntryPoint", - urlTemplate: `${baseUrl}/copy-pasta?search={search_term_string}`, - }, - "query-input": "required name=search_term_string", - } as QueryAction, - }; -} - -export const actionMap: EngagementActionRecord = { - [EngagementAction.CreateCopyPasta]: { action: "create", type: "copyPasta" }, - [EngagementAction.ApproveCopyPasta]: { action: "approve", type: "copyPasta" }, - [EngagementAction.GiveReaction]: { action: "give", type: "reaction" }, - [EngagementAction.RemoveReaction]: { action: "remove", type: "reaction" }, - [EngagementAction.CreateCollection]: { action: "create", type: "collection" }, - [EngagementAction.DeleteCollection]: { action: "delete", type: "collection" }, -}; - -export function handleEngagementAction( - action: EngagementAction, - resourceId: string | null, -): EngagementActionDataDb { - const actionData = actionMap[action]; - - return { - engagementType: action, - action: actionData.action, - id: resourceId, - type: actionData.type, - }; -} - -export function capitalize(str: string): string { - if (!str) return ""; - return str.charAt(0).toUpperCase() + str.slice(1); -} - -export function parseEngagementLogs(log: EngagementActionDataDb) { - let action = ""; - let activity = ""; - - switch (log.action) { - case "create": - action = "Membuat"; - break; - case "approve": - action = "disetujui"; - break; - case "delete": - action = "Menghapus"; - break; - case "give": - action = "Memberikan"; - break; - case "remove": - action = "Menghapus"; - break; - } - - switch (log.type) { - case "collection": - activity = "koleksi"; - break; - case "copyPasta": - activity = "template"; - break; - case "reaction": - activity = "reaksi pada template"; - break; - } - - if (log.action === "approve") { - return { - text: `${capitalize(activity)} ${action}`, - action: `link`, - }; - } - - return { - text: `${action} ${activity}`, - action: `link`, - }; -} - -export function formatNumber(number: number) { - return Intl.NumberFormat("en-US", { - style: "decimal", - useGrouping: true, - notation: "compact", - compactDisplay: "short", - }).format(number); -} diff --git a/src/server/api/routers/collection.ts b/src/server/api/routers/collection.ts index a45bc90..7a7db50 100644 --- a/src/server/api/routers/collection.ts +++ b/src/server/api/routers/collection.ts @@ -1,6 +1,6 @@ import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { handleEngagementAction } from "~/lib/utils"; +import { handleEngagementAction } from "~/utils"; import { createTRPCRouter, @@ -12,6 +12,7 @@ import { editCollectionForm, } from "~/server/form/collection"; import { updateUserEngagementScore } from "~/server/util/db"; +import { checkAndGrantCollectionCuratorAchievement } from "~/server/util/achievement"; export const collectionRouter = createTRPCRouter({ list: publicProcedure @@ -97,6 +98,10 @@ export const collectionRouter = createTRPCRouter({ }); const payload = handleEngagementAction("CreateCollection", collection.id); await updateUserEngagementScore(ctx.db, ctx.session.user.id, payload); + await checkAndGrantCollectionCuratorAchievement( + ctx.db, + ctx.session.user.id, + ); return collection.id; }), @@ -235,6 +240,11 @@ export const collectionRouter = createTRPCRouter({ }, }, }); + + await checkAndGrantCollectionCuratorAchievement( + ctx.db, + ctx.session.user.id, + ); }), delete: protectedProcedure diff --git a/src/server/api/routers/copyPasta.ts b/src/server/api/routers/copyPasta.ts index b0f80f9..faf939d 100644 --- a/src/server/api/routers/copyPasta.ts +++ b/src/server/api/routers/copyPasta.ts @@ -9,12 +9,20 @@ import { publicProcedure, } from "~/server/api/trpc"; -import { getRandomElement, handleEngagementAction } from "~/lib/utils"; +import { + getJakartaDate, + getRandomElement, + handleEngagementAction, +} from "~/utils"; import { type CopyPastaOnlyContent, type CopyPastaSearchResult, } from "~/lib/interface"; import { updateUserEngagementScore } from "~/server/util/db"; +import { + checkAndGrantFiveCopyPastaADayAchievement, + checkAndGrantTagCollectionAchievement, +} from "~/server/util/achievement"; function tokenize(content: string) { return content.toLowerCase().split(/\s+/); @@ -85,8 +93,22 @@ export const copyPastaRouter = createTRPCRouter({ }, }); + await ctx.db.user.update({ + where: { + id: ctx.session.user.id, + }, + data: { + lastPostedAt: getJakartaDate(), + }, + }); + const payload = handleEngagementAction("CreateCopyPasta", copyPasta.id); - await updateUserEngagementScore(ctx.db, ctx.session.user.id, payload); + await Promise.all([ + updateUserEngagementScore(ctx.db, ctx.session.user.id, payload), + checkAndGrantTagCollectionAchievement(ctx.db, ctx.session.user.id), + checkAndGrantFiveCopyPastaADayAchievement(ctx.db, ctx.session.user.id), + ]); + if (isSuperAdmin) { const payload = handleEngagementAction( "ApproveCopyPasta", diff --git a/src/server/api/routers/cron.ts b/src/server/api/routers/cron.ts index 601657e..ed9ea68 100644 --- a/src/server/api/routers/cron.ts +++ b/src/server/api/routers/cron.ts @@ -2,7 +2,7 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "../trpc"; import { TRPCError } from "@trpc/server"; import { env } from "~/env"; -import { getJakartaDate, getJakartaDateString } from "~/lib/utils"; +import { getJakartaDate, getJakartaDateString } from "~/utils"; import { type CreateEmailOptions } from "resend"; import StreakPage from "~/app/_components/Email/Streak"; import { type User } from "@prisma/client"; diff --git a/src/server/api/routers/dashboard.ts b/src/server/api/routers/dashboard.ts index 165c8e8..04c66d6 100644 --- a/src/server/api/routers/dashboard.ts +++ b/src/server/api/routers/dashboard.ts @@ -12,7 +12,7 @@ import { editCopyPastaForm } from "~/server/form/copyPasta"; import { deleteBucketFile } from "~/server/util/storage"; import { editProfile } from "~/server/form/user"; import { updateUserEngagementScore } from "~/server/util/db"; -import { handleEngagementAction } from "~/lib/utils"; +import { handleEngagementAction } from "~/utils"; export const dashboardRouter = createTRPCRouter({ list: protectedProcedure diff --git a/src/server/api/routers/reaction.ts b/src/server/api/routers/reaction.ts index c143a85..59cf404 100644 --- a/src/server/api/routers/reaction.ts +++ b/src/server/api/routers/reaction.ts @@ -7,7 +7,7 @@ import { import { EmotionType } from "@prisma/client"; import { redirect } from "next/navigation"; import { updateUserEngagementScore } from "~/server/util/db"; -import { handleEngagementAction } from "~/lib/utils"; +import { handleEngagementAction } from "~/utils"; export const reactionRouter = createTRPCRouter({ reactionByCopyPastaId: protectedProcedureLimited diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index 0fc289b..9a80543 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -1,7 +1,7 @@ import { AchievementType, type PrismaClient, type User } from "@prisma/client"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; -import { getJakartaDate, getJakartaDateString } from "~/lib/utils"; +import { getJakartaDate, getJakartaDateString } from "~/utils"; import { createTRPCRouter, protectedProcedure, @@ -41,7 +41,6 @@ export async function updateUserStreak(userId: string, db: PrismaClient) { data: { currentStreak: newStreak, longestStreak: Math.max(newStreak, user.longestStreak || 0), - lastPostedAt: now, }, }); diff --git a/src/server/util/achievement.ts b/src/server/util/achievement.ts new file mode 100644 index 0000000..acbbbd8 --- /dev/null +++ b/src/server/util/achievement.ts @@ -0,0 +1,126 @@ +import { AchievementType, type PrismaClient } from "@prisma/client"; +import { ACHIEVEMENT_MINIMUM } from "~/lib/constant"; +import { getJakartaDate } from "~/utils"; + +export async function checkAndGrantFiveCopyPastaADayAchievement( + db: PrismaClient, + userId: string, +) { + const today = getJakartaDate(); + today.setHours(0, 0, 0); + + const userCreatedCopyPastaToday = await db.copyPasta.count({ + where: { + createdAt: { + gte: today, + }, + createdById: userId, + }, + }); + + if (userCreatedCopyPastaToday >= 5) { + const existingAchievement = await db.achievement.findUnique({ + where: { + userId_type: { + userId, + type: AchievementType.FiveCopyPastaADay, + }, + }, + }); + + if (!existingAchievement) { + await db.achievement.create({ + data: { + userId, + type: AchievementType.FiveCopyPastaADay, + }, + }); + console.log( + `User ${userId} has been granted the FiveCopyPastaADay achievement!`, + ); + } + } +} + +export async function checkAndGrantTagCollectionAchievement( + db: PrismaClient, + userId: string, +) { + const distinctTags = await db.copyPastasOnTags.findMany({ + where: { + copyPastas: { + createdById: userId, + }, + }, + distinct: ["tagId"], + }); + + if (distinctTags.length >= ACHIEVEMENT_MINIMUM.TAG_COLLECTOR_MINIMUM) { + const existingAchievement = await db.achievement.findUnique({ + where: { + userId_type: { + userId, + type: AchievementType.TagCollector, + }, + }, + }); + + if (!existingAchievement) { + await db.achievement.create({ + data: { + userId, + type: AchievementType.TagCollector, + }, + }); + console.log(`User ${userId} has been granted the TagMaster achievement!`); + } + } +} + +export async function checkAndGrantCollectionCuratorAchievement( + db: PrismaClient, + userId: string, +) { + const collections = await db.collection.findMany({ + where: { + createdById: userId, + }, + include: { + _count: { + select: { copyPastas: true }, + }, + }, + }); + + const qualifiedCollections = collections.filter( + (collection) => + collection._count.copyPastas >= + ACHIEVEMENT_MINIMUM.COLLECTION_CURATOR.COPY_PASTA_PER_COLLECTION, + ); + + if ( + qualifiedCollections.length >= + ACHIEVEMENT_MINIMUM.COLLECTION_CURATOR.COLLECTION_MINIMUM + ) { + const existingAchievement = await db.achievement.findUnique({ + where: { + userId_type: { + userId, + type: AchievementType.CollectionCurator, + }, + }, + }); + + if (!existingAchievement) { + await db.achievement.create({ + data: { + userId, + type: AchievementType.CollectionCurator, + }, + }); + console.log( + `User ${userId} has been granted the CollectionCurator achievement!`, + ); + } + } +} diff --git a/src/utils/data.ts b/src/utils/data.ts new file mode 100644 index 0000000..5466635 --- /dev/null +++ b/src/utils/data.ts @@ -0,0 +1,49 @@ +import { type $Enums } from "@prisma/client"; + +export function getRandomElement(array: string[]) { + if (array.length === 0) { + throw new Error("Array cannot be empty"); + } + return array[Math.floor(Math.random() * array.length)]; +} + +export function mergeReactions( + data?: { + copyPastaId: string; + userId: string; + emotion: $Enums.EmotionType; + _count: { + emotion: number; + }; + }[], +) { + return data?.reduce( + (acc, curr) => { + const key = `${curr.emotion}`; + + if (!acc[key]) { + acc[key] = { + ...curr, + userIds: [], + _count: { emotion: 0 }, + }; + } + + acc[key]._count.emotion += curr._count.emotion; + acc[key].userIds.push(curr.userId); + + return acc; + }, + {} as Record< + string, + { + copyPastaId: string; + userIds: string[]; + emotion: $Enums.EmotionType; + _count: { + emotion: number; + }; + } + >, + ); +} diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 0000000..c0f378f --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,24 @@ +import { format, formatDistance, parse } from "date-fns"; +import { format as formatTz, toZonedTime } from "date-fns-tz"; +import { id } from "date-fns/locale"; +import { TIMEZONE } from "../lib/constant"; + +export function formatDateToHuman(date: Date, formatString = "PPP") { + return format(date, formatString, { locale: id }); +} + +export function formatDateDistance(date: Date) { + return formatDistance(date, new Date(), { locale: id, addSuffix: true }); +} + +export function parseDate(date: string) { + return parse(date, "d MMMM yyyy", new Date(), { locale: id }); +} + +export function getJakartaDate(date: Date = new Date()): Date { + return toZonedTime(date, TIMEZONE); +} + +export function getJakartaDateString(date: Date = new Date()): string { + return formatTz(getJakartaDate(date), "yyyy-MM-dd", { timeZone: TIMEZONE }); +} diff --git a/src/utils/engagement.ts b/src/utils/engagement.ts new file mode 100644 index 0000000..f02c0d5 --- /dev/null +++ b/src/utils/engagement.ts @@ -0,0 +1,76 @@ +import { EngagementAction } from "@prisma/client"; +import { + type EngagementActionRecord, + type EngagementActionDataDb, +} from "../lib/interface"; +import { capitalize } from "./string"; + +export const actionMap: EngagementActionRecord = { + [EngagementAction.CreateCopyPasta]: { action: "create", type: "copyPasta" }, + [EngagementAction.ApproveCopyPasta]: { action: "approve", type: "copyPasta" }, + [EngagementAction.GiveReaction]: { action: "give", type: "reaction" }, + [EngagementAction.RemoveReaction]: { action: "remove", type: "reaction" }, + [EngagementAction.CreateCollection]: { action: "create", type: "collection" }, + [EngagementAction.DeleteCollection]: { action: "delete", type: "collection" }, +}; + +export function handleEngagementAction( + action: EngagementAction, + resourceId: string | null, +): EngagementActionDataDb { + const actionData = actionMap[action]; + + return { + engagementType: action, + action: actionData.action, + id: resourceId, + type: actionData.type, + }; +} + +export function parseEngagementLogs(log: EngagementActionDataDb) { + let action = ""; + let activity = ""; + + switch (log.action) { + case "create": + action = "Membuat"; + break; + case "approve": + action = "disetujui"; + break; + case "delete": + action = "Menghapus"; + break; + case "give": + action = "Memberikan"; + break; + case "remove": + action = "Menghapus"; + break; + } + + switch (log.type) { + case "collection": + activity = "koleksi"; + break; + case "copyPasta": + activity = "template"; + break; + case "reaction": + activity = "reaksi pada template"; + break; + } + + if (log.action === "approve") { + return { + text: `${capitalize(activity)} ${action}`, + action: `link`, + }; + } + + return { + text: `${action} ${activity}`, + action: `link`, + }; +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..4004a7b --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,7 @@ +export * from "./date"; +export * from "./string"; +export * from "./ui"; +export * from "./url"; +export * from "./data"; +export * from "./schema"; +export * from "./engagement"; diff --git a/src/utils/schema.ts b/src/utils/schema.ts new file mode 100644 index 0000000..81c4deb --- /dev/null +++ b/src/utils/schema.ts @@ -0,0 +1,32 @@ +import { + type Thing, + type WithContext, + type WebSite, + type SearchAction, +} from "schema-dts"; +import { baseUrl } from "../lib/constant"; + +export function generateSchemaById(json: WithContext) { + return json; +} + +type QueryAction = SearchAction & { + "query-input": string; +}; + +export function generateSchemaOrgWebSite(): WithContext { + return { + "@context": "https://schema.org", + "@type": "WebSite", + name: "Arsip Template", + url: baseUrl, + potentialAction: { + "@type": "SearchAction", + target: { + "@type": "EntryPoint", + urlTemplate: `${baseUrl}/copy-pasta?search={search_term_string}`, + }, + "query-input": "required name=search_term_string", + } as QueryAction, + }; +} diff --git a/src/utils/string.ts b/src/utils/string.ts new file mode 100644 index 0000000..72026d0 --- /dev/null +++ b/src/utils/string.ts @@ -0,0 +1,37 @@ +import he from "he"; +import { type EnrichedTweet } from "react-tweet"; + +export function sanitizeTweet(text: string) { + return he.decode(text); +} + +export function sanitizeTweetEnrich(tweet: EnrichedTweet) { + return tweet.entities + .map((e) => { + switch (e.type) { + case "media": + case "symbol": + case "url": + return; + case "hashtag": + case "mention": + case "text": + return e.text; + } + }) + .join(" "); +} + +export function trimContent(content: string, length = 255) { + if (!content) { + return "😱😱😱"; + } + + const trimmedContent = content.slice(0, length).trimEnd(); + return trimmedContent + (content.length > length ? "..." : ""); +} + +export function capitalize(str: string): string { + if (!str) return ""; + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/src/utils/ui.ts b/src/utils/ui.ts new file mode 100644 index 0000000..90355cd --- /dev/null +++ b/src/utils/ui.ts @@ -0,0 +1,28 @@ +import { twMerge } from "tailwind-merge"; +import { type ClassValue, clsx } from "clsx"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function getMedal(number: number) { + switch (number) { + case 1: + return "🥇 "; + case 2: + return "🥈 "; + case 3: + return "🥉 "; + default: + return number; + } +} + +export function formatNumber(number: number) { + return Intl.NumberFormat("en-US", { + style: "decimal", + useGrouping: true, + notation: "compact", + compactDisplay: "short", + }).format(number); +} diff --git a/src/utils/url.ts b/src/utils/url.ts new file mode 100644 index 0000000..e231981 --- /dev/null +++ b/src/utils/url.ts @@ -0,0 +1,46 @@ +import { OriginSource } from "@prisma/client"; +import { type Breadcrumb } from "~/lib/interface"; + +export function getTweetId(tweetUrl: string) { + const match = tweetUrl.match(/\/status\/(\d+)/); + return match ? match[1] : undefined; +} + +export function determineSource(url = "Other") { + const regex = + /((https?:\/\/)?(?:www\.)?(facebook\.com|fb\.me|twitter\.com|t\.co|x\.com)\/[^\s]+)/; + if (url === "Other") { + return OriginSource.Other; + } + const match = RegExp(regex).exec(url.toLowerCase()); + if (!match) { + return OriginSource.Other; + } + const source = match[3]; + + if (source === "facebook.com" || source === "fb.me") { + return OriginSource.Facebook; + } else if ( + source === "twitter.com" || + source === "x.com" || + source === "t.co" + ) { + return OriginSource.Twitter; + } else { + return OriginSource.Other; + } +} + +export function getBreadcrumbs(url: string): Breadcrumb[] { + const parts = url.split("/").filter((part) => part !== ""); + const breadcrumbs: Breadcrumb[] = []; + let currentPath = ""; + + parts.forEach((part) => { + currentPath += `/${part}`; + const text = part; + breadcrumbs.push({ url: currentPath, text }); + }); + + return breadcrumbs; +}