Skip to content

Commit

Permalink
Added report system
Browse files Browse the repository at this point in the history
  • Loading branch information
darklight9811 committed Jul 21, 2024
1 parent 86acf58 commit 7ec53a8
Show file tree
Hide file tree
Showing 23 changed files with 400 additions and 53 deletions.
52 changes: 47 additions & 5 deletions apps/app/src/app/[locale]/(app)/entities/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,26 @@ import Share from "@/components/share";
import { Link } from "@/lib/navigation";
import parallel from "@/lib/parallel";
import { baseUrl } from "@/lib/url";
import ReportDialog from "@/modules/report/components/report-dialog";
import { currentUser } from "@/modules/user/loaders";
import { Avatar, AvatarFallback, AvatarImage } from "@repo/ds/ui/avatar";
import { Button, buttonVariants } from "@repo/ds/ui/button";
import { Carousel, CarouselContent, CarouselItem } from "@repo/ds/ui/carousel";
import { Tooltip, TooltipContent, TooltipTrigger } from "@repo/ds/ui/tooltip";
import { env } from "@repo/env";
import entityService from "@repo/services/entity";
import { Edit, Flag, Printer, Share2, Trash, User } from "lucide-react";
import {
CircleAlert,
Edit,
Flag,
Printer,
Share2,
Trash,
User,
} from "lucide-react";
import type { Metadata } from "next";
import { getLocale, getTranslations } from "next-intl/server";
import { headers } from "next/headers";
import Image from "next/image";
import { notFound } from "next/navigation";
import { FindDialog } from "./_components/find-dialog";
Expand Down Expand Up @@ -57,6 +68,12 @@ export default async function Page({ params }: { params: { id: string } }) {

if (!data) return notFound();

const ip = headers().get("x-ip");
const canReport = !(
data.reports.find((t) => t.id_user_created === user?.id) ||
data.reports.find((t) => t.ip === ip)
);

return (
<main className="p-4 flex flex-col md:flex-row grow container gap-2">
<div className="md:max-w-[280px] w-full">
Expand All @@ -67,7 +84,17 @@ export default async function Page({ params }: { params: { id: string } }) {
</AvatarFallback>
</Avatar>

<h1 className="font-bold">{data.name}</h1>
<h1 className="font-bold flex gap-1">
{data.reports.length > 1 && (
<Tooltip>
<TooltipContent>{t("reported")}</TooltipContent>
<TooltipTrigger asChild>
<CircleAlert color="red" />
</TooltipTrigger>
</Tooltip>
)}{" "}
{data.name}
</h1>

<div>
{t("date", { date: data.date_created?.toLocaleDateString(locale) })}
Expand All @@ -79,9 +106,24 @@ export default async function Page({ params }: { params: { id: string } }) {
{t("found")}
</Button>
</FindDialog>
<Button type="button" variant="destructive" disabled>
<Flag />
</Button>
<Tooltip>
{!canReport && (
<TooltipContent>{t("already_reported")}</TooltipContent>
)}
<TooltipTrigger asChild>
<div>
<ReportDialog data={{ id_entity: data.id }}>
<Button
type="button"
variant="destructive"
disabled={!canReport}
>
<Flag />
</Button>
</ReportDialog>
</div>
</TooltipTrigger>
</Tooltip>
</div>

<div className="flex gap-2 my-2 justify-between">
Expand Down
7 changes: 7 additions & 0 deletions apps/app/src/app/[locale]/(app)/entities/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export default async function Page(props: { searchParams: PaginationType }) {
</Link>
</div>
)}

{data.length > 0 &&
Array.from(new Array((pagination.limit - data.length) % 4)).map(
(x) => (
<div key={x} className="max-w-[47%] sm:max-w-[210px] w-full" />
),
)}
</div>

<Pagination page={pagination.page || 1} pages={pagination.pages} />
Expand Down
5 changes: 4 additions & 1 deletion apps/app/src/app/[locale]/_components/client-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import { Toaster } from "@repo/ds/ui/toast";
import { TooltipProvider } from "@repo/ds/ui/tooltip";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { useState } from "react";
Expand All @@ -12,7 +14,8 @@ export default function ClientProvider({
return (
<QueryClientProvider client={client}>
<ReactQueryDevtools />
{children}
<Toaster />
<TooltipProvider delayDuration={300}>{children}</TooltipProvider>
</QueryClientProvider>
);
}
2 changes: 2 additions & 0 deletions apps/app/src/lib/parallel.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { currentUser } from "@/modules/user/loaders";
import type Metadata from "@repo/services/types/metadata";
import { headers } from "next/headers";

export async function buildMetadata() {
return {
user: await currentUser(),
ip: headers().get("x-ip") as string,
} satisfies Metadata;
}

Expand Down
3 changes: 2 additions & 1 deletion apps/app/src/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ const ratelimit = new Ratelimit({

const clerk = clerkMiddleware(
async (auth, req) => {
const ip = ipAddress(req) || "127.0.0.1";
req.headers.set("x-pathname", req.nextUrl.pathname);
req.headers.set("x-ip", ip);

// we generate the webmanifest dinamically to allow internacionalization
if (req.nextUrl.pathname === "/manifest.webmanifest") {
Expand All @@ -59,7 +61,6 @@ const clerk = clerkMiddleware(
// assets are always public and not internacionalized
if (isAssetRoute(req)) return NextResponse.next();

const ip = ipAddress(req) || "127.0.0.1";
const { success } = await ratelimit.limit(ip);

// make sure the user is not rate limited or not in the rate limit screen
Expand Down
13 changes: 13 additions & 0 deletions apps/app/src/modules/report/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"use server";

import api from "@/lib/api";
import { buildMetadata } from "@/lib/parallel";
import report from "@repo/schemas/report";
import reportService from "@repo/services/report";

export const store = api
.zod(report)
.service(reportService.store, buildMetadata)
.action(async () => ({
replace: true,
}));
124 changes: 124 additions & 0 deletions apps/app/src/modules/report/components/report-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"use client";

import Field from "@repo/ds/form/field";
import Form from "@repo/ds/form/form";
import tailwind from "@repo/ds/tw";
import { Button } from "@repo/ds/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@repo/ds/ui/dialog";
import { Label } from "@repo/ds/ui/label";
import { RadioGroup, RadioGroupItem } from "@repo/ds/ui/radio-group";
import { Textarea } from "@repo/ds/ui/textarea";
import report from "@repo/schemas/report";
import { CircleCheck } from "lucide-react";
import { useTranslations } from "next-intl";
import { store } from "../actions";

/**
* ### MARK: Interfaces
*/

interface Props {
children: React.ReactNode;

data?: { id_entity?: string };
}

/**
* ### MARK: Component
*/

export default function ReportDialog(props: Props) {
const t = useTranslations("report");

return (
<Dialog>
<DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t("title")}</DialogTitle>
<DialogDescription>{t("subtitle")}</DialogDescription>
</DialogHeader>

<Form
schema={report}
onSubmit={store}
data={props.data}
replace={Success}
>
<Field
name="reason"
render={({ field }) => (
<RadioGroup
{...field}
onValueChange={field.onChange}
className="border border-border rounded-lg gap-0"
>
<div className="flex items-center space-x-2 p-4 border-b border-border">
<RadioGroupItem value="offensive" id="offensive" />
<Label htmlFor="offensive">{t("reason.offensive")}</Label>
</div>
<div className="flex items-center space-x-2 p-4 border-b border-border">
<RadioGroupItem value="not_missing" id="not_missing" />
<Label htmlFor="not_missing">{t("reason.not_missing")}</Label>
</div>
<div className="flex items-center space-x-2 p-4 border-b border-border">
<RadioGroupItem value="ownership" id="ownership" />
<Label htmlFor="ownership">{t("reason.ownership")}</Label>
</div>
<div className="flex items-center space-x-2 p-4">
<RadioGroupItem value="other" id="other" />
<Label htmlFor="other">{t("reason.other")}</Label>
</div>
</RadioGroup>
)}
/>

<Field
name="description"
label={t("description")}
render={({ field }) => <Textarea {...field} />}
/>

<DialogFooter>
<Button variant="destructive" type="submit">
{t("action")}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}

/**
* ### MARK: Helpers
*/

function Success() {
const t = useTranslations("report");

return (
<div className="flex flex-col justify-center items-center gap-4 animate-left-in">
<CircleCheck
size={128}
color={tailwind.theme.extend.colors.primary.DEFAULT}
/>

<p>{t("success")}</p>

<DialogFooter>
<DialogTrigger asChild>
<Button>{t("close")}</Button>
</DialogTrigger>
</DialogFooter>
</div>
);
}
15 changes: 15 additions & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json",
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"overrides": [
{
"include": ["./packages/config/eslint/general.json"],
"ignore": ["./next"]
}
]
}
Binary file modified bun.lockb
Binary file not shown.
4 changes: 4 additions & 0 deletions packages/ds/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,24 @@
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.0",
"@radix-ui/react-select": "^2.1.1",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
"embla-carousel-react": "^8.1.6",
"lucide-react": "^0.394.0",
"next": "^14.2.4",
"next-intl": "^3.15.2",
"next-themes": "^0.3.0",
"react": "^18.0.0",
"react-day-picker": "^8.10.1",
"react-dom": "^18.0.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.52.0",
"sonner": "^1.5.0",
"tailwind-merge": "^2.2.2",
"vaul": "^0.9.1",
"zod": "^3.22.4"
Expand Down
28 changes: 15 additions & 13 deletions packages/ds/src/components/form/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@ import { usePathname, useRouter } from "next/navigation";
import { FormProvider, useForm } from "react-hook-form";
import { objectToFormData } from "../../lib/form";

import type React from "react";
import { createElement, useState } from "react";
import type { ZodSchema } from "zod";

interface FormProps {
onSubmit?: (
data: FormData,
raw: Record<string, unknown>,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
) => any | Promise<any>;
onSubmit?: (data: any, raw: Record<string, unknown>) => any | Promise<any>;
onChange?: (
data: FormData,
raw: Record<string, unknown>,
Expand All @@ -23,11 +21,13 @@ interface FormProps {
schema?: ZodSchema;
data?: Record<string, unknown>;
defaultData?: Record<string, unknown>;
replace?: (data: any) => React.ReactNode;
}

export default function Form(props: FormProps) {
const router = useRouter();
const pathname = usePathname();
const [replace, setreplace] = useState<any | undefined>(undefined);
const form = useForm({
defaultValues: props.data || props.defaultData,
resolver: props.schema && zodResolver(props.schema),
Expand All @@ -37,13 +37,12 @@ export default function Form(props: FormProps) {
<FormProvider {...form}>
<form
onChange={
props.onChange
? () =>
props.onChange?.(
objectToFormData(form.getValues()),
form.getValues(),
)
: undefined
props.onChange &&
(() =>
props.onChange?.(
objectToFormData(form.getValues()),
form.getValues(),
))
}
onSubmit={form.handleSubmit(async (payload) => {
if (!props.onSubmit) return;
Expand All @@ -65,12 +64,15 @@ export default function Form(props: FormProps) {

if (data.redirect) return router.push(data.redirect);
if (data.reload) return router.push(pathname);
if (data.replace && props.replace) return setreplace(data);

return props?.onSuccess?.(data);
})}
className={props.className}
>
{props.children}
{replace
? createElement(props.replace as () => React.ReactNode, replace)
: props.children}
</form>
</FormProvider>
);
Expand Down
Loading

0 comments on commit 7ec53a8

Please sign in to comment.