Skip to content

Commit

Permalink
[MD-5019] Implement Global Filter Selector (#3)
Browse files Browse the repository at this point in the history
* Added TRPC and global filter component/feature

* Address Jack's PR comments on 10/14

* Added vercel deployment for this repo

Remove unused var

@/api path to @isomorphic/api since it's now a separate package

Fix build errors

Add env in turbo.json
  • Loading branch information
montayrekj authored Oct 18, 2024
1 parent 759072d commit 4edba6c
Show file tree
Hide file tree
Showing 25 changed files with 1,279 additions and 211 deletions.
1 change: 1 addition & 0 deletions apps/isomorphic-i18n/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
11 changes: 11 additions & 0 deletions apps/isomorphic-i18n/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"clean": "rm -rf node_modules .next .cache .turbo"
},
"dependencies": {
"@isomorphic/api": "workspace:^0.1.0",
"@deck.gl/aggregation-layers": "^9.0.23",
"@deck.gl/core": "^9.0.23",
"@deck.gl/layers": "^9.0.23",
Expand Down Expand Up @@ -43,6 +44,11 @@
"@react-email/text": "0.0.8",
"@t3-oss/env-nextjs": "^0.10.1",
"@tailwindcss/container-queries": "^0.1.1",
"@tanstack/react-query": "^5.59.9",
"@tanstack/react-query-devtools": "^5.59.9",
"@trpc/client": "11.0.0-next-beta.264",
"@trpc/react-query": "11.0.0-next-beta.264",
"@trpc/server": "11.0.0-next-beta.264",
"@uploadthing/react": "^6.5.1",
"accept-language": "^3.0.18",
"clsx": "^2.1.1",
Expand All @@ -53,11 +59,13 @@
"eslint-config-next": "14.2.3",
"framer-motion": "^11.1.9",
"fs-extra": "^11.2.0",
"fuse.js": "^7.0.0",
"glob": "^10.3.12",
"i18next": "^23.11.4",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-resources-to-backend": "^1.2.1",
"jotai": "2.8.2",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
"mapbox-gl": "^3.5.2",
"maplibre-gl": "^4.5.0",
Expand Down Expand Up @@ -105,8 +113,10 @@
"rsuite": "^5.67.0",
"sharp": "^0.33.3",
"simplebar-react": "^3.2.5",
"superjson": "^2.2.1",
"swiper": "^11.1.1",
"tailwind-merge": "^2.3.0",
"trpc-openapi": "^1.2.0",
"typescript": "5.4.5",
"uploadthing": "^6.10.1",
"zod": "^3.23.7"
Expand All @@ -121,6 +131,7 @@
"@total-typescript/ts-reset": "^0.5.1",
"@types/date-arithmetic": "^4.1.4",
"@types/google.maps": "^3.55.8",
"@types/js-cookie": "^3.0.6",
"@types/lodash": "^4.17.1",
"@types/node": "20.12.10",
"@types/nodemailer": "^6.4.15",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ const pageHeader = {
],
};

export default function NewPage({ lang }: { lang?: string }) {
export default function NewPage({
params: { lang },
}: {
params: { lang?: string };
}) {
return (
<div className="flex flex-col h-screen">
<PageHeader title={pageHeader.title} breadcrumb={pageHeader.breadcrumb} />
Expand Down
30 changes: 16 additions & 14 deletions apps/isomorphic-i18n/src/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { inter, lexendDeca } from "@/app/fonts";
import cn from "@utils/class-names";
import { dir } from "i18next";
import { languages } from "../i18n/settings";
import { GlobalFilterProvider } from "../components/global-filter-provider";
import { TRPCReactProvider } from "@/trpc/react";

const NextProgress = dynamic(() => import("@components/next-progress"), {
ssr: false,
Expand All @@ -34,24 +36,24 @@ export default async function RootLayout({
}) {
const session = await getServerSession(authOptions);
return (
<html
lang={lang}
dir={dir(lang)}
suppressHydrationWarning
>
<html lang={lang} dir={dir(lang)} suppressHydrationWarning>
<body
suppressHydrationWarning
className={cn(inter.variable, lexendDeca.variable, "font-inter")}
>
<AuthProvider session={session}>
<ThemeProvider>
<NextProgress />
{children}
<Toaster />
<GlobalDrawer />
<GlobalModal />
</ThemeProvider>
</AuthProvider>
<GlobalFilterProvider>
<TRPCReactProvider>
<AuthProvider session={session}>
<ThemeProvider>
<NextProgress />
{children}
<Toaster />
<GlobalDrawer />
<GlobalModal />
</ThemeProvider>
</AuthProvider>
</TRPCReactProvider>
</GlobalFilterProvider>
</body>
</html>
);
Expand Down
61 changes: 33 additions & 28 deletions apps/isomorphic-i18n/src/app/api/aggregated-catch/route.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,53 @@
import { NextRequest, NextResponse } from 'next/server';
import clientPromise from '@/app/mongodb';
import { NextRequest, NextResponse } from "next/server";
import clientPromise from "@/app/mongodb";

export async function GET(req: NextRequest) {
try {
const client = await clientPromise;
const db = client.db('kenya');
const collection = db.collection('legacy_data');

// Filter and aggregate data
const data = await collection.aggregate([
try {
const client = await clientPromise;
const db = client.db("kenya");
const collection = db.collection("legacy_data");

// Filter and aggregate data
const data = await collection
.aggregate([
{
$match: {
landing_site: "Kenyatta"
}
landing_site: "Kenyatta",
},
},
{
$project: {
landing_date: {
$dateTrunc: {
date: "$landing_date",
unit: "month"
}
unit: "month",
},
},
fish_category: 1,
catch_kg: 1
}
catch_kg: 1,
},
},
{
$group: {
_id: {
landing_date: "$landing_date",
fish_category: "$fish_category"
fish_category: "$fish_category",
},
catch_kg: { $sum: "$catch_kg" }
}
catch_kg: { $sum: "$catch_kg" },
},
},
{
$sort: { "_id.landing_date": 1, "_id.fish_category": 1 }
}
]).toArray();

return NextResponse.json(data);
} catch (error) {
console.error('Error fetching data:', (error as Error).message);
return NextResponse.json({ error: 'Internal Server Error', details: (error as Error).message }, { status: 500 });
}
}
$sort: { "_id.landing_date": 1, "_id.fish_category": 1 },
},
])
.toArray();

return NextResponse.json(data);
} catch (error) {
console.error("Error fetching data:", (error as Error).message);
return NextResponse.json(
{ error: "Internal Server Error", details: (error as Error).message },
{ status: 500 }
);
}
}
43 changes: 43 additions & 0 deletions apps/isomorphic-i18n/src/app/api/trpc/[trpc]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { appRouter, createTRPCContext } from "@isomorphic/api";
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";

/**
* Configure basic CORS headers
* You should extend this to match your needs
*/
function setCorsHeaders(res: Response) {
res.headers.set("Access-Control-Allow-Origin", "*");
res.headers.set("Access-Control-Request-Method", "*");
res.headers.set("Access-Control-Allow-Methods", "OPTIONS, GET, POST");
res.headers.set("Access-Control-Allow-Headers", "*");
}

export function OPTIONS() {
const response = new Response(null, {
status: 204,
});
setCorsHeaders(response);
return response;
}

const handler = async (req: any) => {
const response = await fetchRequestHandler({
endpoint: "/api/trpc",
router: appRouter,
req,
createContext: () => {
return createTRPCContext({
session: req.auth,
headers: req.headers,
});
},
onError({ error, path }) {
console.error(`>>> tRPC Error on '${path}'`, error);
},
});

setCorsHeaders(response);
return response;
};

export { handler as GET, handler as POST };
140 changes: 140 additions & 0 deletions apps/isomorphic-i18n/src/app/components/filter-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { ActionIcon, Checkbox, Input, Popover } from "rizzui";
import { TbFilterCog } from "react-icons/tb";
import { ChangeEvent, useEffect, useMemo, useState } from "react";
import { BmuType, useGlobalFilter } from "./global-filter-provider";
import Fuse from "fuse.js";

export const FilterSelector = () => {
const [searchFilter, setSearchFilter] = useState("");
const [filteredList, setFilteredList] = useState<BmuType[] | string[]>([]);
const [isOpen, setIsOpen] = useState(false);
const { bmuOriginalData, bmuFilter, setBmuFilter } = useGlobalFilter();

const fuse = new Fuse(
bmuOriginalData.flatMap((section) =>
section.units.map((unit) => unit.value)
),
{
includeScore: true,
}
);

useEffect(() => {
setFilteredList(bmuOriginalData);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const handleSearchChange = (e: ChangeEvent<HTMLInputElement>) => {
setSearchFilter(e.target.value);
if (e.target.value) {
const result = fuse.search(e.target.value);
setFilteredList(result.map((res) => res.item));
} else {
setFilteredList(bmuOriginalData);
}
};

return (
<Popover isOpen={isOpen} setIsOpen={setIsOpen} placement="bottom-end">
<Popover.Trigger>
<ActionIcon variant="text" className="relative">
<TbFilterCog className="h-6 w-6 fill-[#D6D6D6] [stroke-width:1.5px]" />
</ActionIcon>
</Popover.Trigger>
<Popover.Content className="w-[350px]">
<Input
placeholder="Search here..."
value={searchFilter}
onChange={handleSearchChange}
/>
<div className="space-y-2 mt-4">
{filteredList.map((section, idx) => {
return (
<FilterGroup
key={`bmu-section-${idx}`}
bmuSection={section}
searchFilter={searchFilter}
/>
);
})}
</div>
</Popover.Content>
</Popover>
);
};

const FilterGroup = ({
bmuSection,
searchFilter,
}: {
bmuSection: BmuType | string;
searchFilter: string;
}) => {
const { bmuFilter, setBmuFilter } = useGlobalFilter();

const handleBmuSelect = (unit: string) => {
if (bmuFilter.includes(unit)) {
setBmuFilter(bmuFilter.filter((filter) => filter !== unit));
} else {
setBmuFilter([...bmuFilter, unit]);
}
};

if (typeof bmuSection === "string" && searchFilter) {
const unit = bmuSection as string;

return (
<Checkbox
key={unit}
label={unit}
checked={bmuFilter.findIndex((filter) => filter === unit) !== -1}
onChange={() => handleBmuSelect(unit)}
/>
);
} else {
const section = bmuSection as BmuType;
const allSelected = section.units.every((unit) => {
return bmuFilter.includes(unit.value);
});

const handleSectionSelect = () => {
if (allSelected) {
setBmuFilter(
bmuFilter.filter(
(filter) =>
!section.units.flatMap((unit) => unit.value).includes(filter)
)
);
} else {
setBmuFilter([
...bmuFilter,
...section.units.map((unit) => unit.value),
]);
}
};

return (
<div>
<Checkbox
label={section.sectionName}
checked={allSelected}
onChange={handleSectionSelect}
/>
<div className="mt-2 ml-8 space-y-2">
{section.units.map((unit) => {
return (
<Checkbox
key={unit.value}
label={unit.value}
checked={
bmuFilter.findIndex((filter) => filter === unit.value) !== -1
}
onChange={() => handleBmuSelect(unit.value)}
/>
);
})}
</div>
</div>
);
}
};
Loading

0 comments on commit 4edba6c

Please sign in to comment.