Skip to content

Commit

Permalink
feat(sort-filter): code restructure to get search term
Browse files Browse the repository at this point in the history
feat(sort-filter): get projects with metadata from server

feat(sort-filter): search by name on the server

fix(frontend): fix small react bugs

fix(frontend): fix favicon.svg call in html meta

fix(main): view projects button as router and not link

fix(form): prevent undefined url call

fix(frontend): activate ssr to prevent hydration errors

fix(search): useDeferredValue to lag behind on search

fix(image): prevent console.log error

fix(image): pr comments

fix(search): single if
  • Loading branch information
NicoSerranoP authored and kittybest committed Jan 10, 2025
1 parent 81179a4 commit a9e5232
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 86 deletions.
2 changes: 1 addition & 1 deletion packages/interface/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ NEXT_PUBLIC_MACI_START_BLOCK=

NEXT_PUBLIC_MACI_SUBGRAPH_URL=

NEXT_PUBLIC_ROUND_LOGO="round-logo.png"
NEXT_PUBLIC_ROUND_LOGO="round-logo.svg"

NEXT_PUBLIC_START_DATE=2024-01-01T00:00:00.000Z
NEXT_PUBLIC_REGISTRATION_END_DATE=2024-01-01T00:00:00.000Z
Expand Down
143 changes: 80 additions & 63 deletions packages/interface/src/components/ImageUpload.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable react/require-default-props */

import { useMutation } from "@tanstack/react-query";
import clsx from "clsx";
import { ImageIcon } from "lucide-react";
import { type ComponentProps, useRef, useCallback } from "react";
import { type ComponentProps, forwardRef, useRef, useCallback, ChangeEvent } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { toast } from "sonner";

Expand All @@ -12,71 +14,86 @@ export interface IImageUploadProps extends ComponentProps<"img"> {
maxSize?: number;
}

export const ImageUpload = ({
name = "",
maxSize = 1024 * 1024, // 1 MB
className,
}: IImageUploadProps): JSX.Element => {
const ref = useRef<HTMLInputElement>(null);
const { control } = useFormContext();
export const ImageUpload = forwardRef(
(
{
name = "",
maxSize = 1024 * 1024, // 1 MB
className,
}: IImageUploadProps,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_,
): JSX.Element => {
const internalRef = useRef<HTMLInputElement>(null);
const { control } = useFormContext();

const select = useMutation({
mutationFn: async (file: File) => {
if (file.size >= maxSize) {
toast.error("Image too large", {
description: `The image to selected is: ${(file.size / 1024).toFixed(2)} / ${(maxSize / 1024).toFixed(2)} kb`,
});
throw new Error("IMAGE_TOO_LARGE");
}

return Promise.resolve(URL.createObjectURL(file));
},
});

const onClickIconButton = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
}, []);

const select = useMutation({
mutationFn: async (file: File) => {
if (file.size >= maxSize) {
toast.error("Image too large", {
description: `The image to selected is: ${(file.size / 1024).toFixed(2)} / ${(maxSize / 1024).toFixed(2)} kb`,
});
throw new Error("IMAGE_TOO_LARGE");
}
const onClickContainer = useCallback(() => {
internalRef.current?.click();
}, [internalRef]);

return Promise.resolve(URL.createObjectURL(file));
},
});
const onChangeInputImage = useCallback(
(event: ChangeEvent<HTMLInputElement>, onChange: (event: string) => void) => {
const [file] = event.target.files ?? [];
if (file) {
select.mutate(file, {
onSuccess: (objectUrl) => {
onChange(objectUrl);
},
});
}
},
[select],
);

const onClickIconButton = useCallback((e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
}, []);
return (
<Controller
control={control}
name={name}
render={({ field: { value, onChange, ...field } }) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div className={clsx("relative cursor-pointer overflow-hidden", className)} onClick={onClickContainer}>
<IconButton className="absolute bottom-1 right-1" icon={ImageIcon} onClick={onClickIconButton} />

return (
<Controller
control={control}
name={name}
render={({ field: { value, onChange, ...field } }) => (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events
<div
className={clsx("relative cursor-pointer overflow-hidden", className)}
onClick={() => ref.current?.click()}
>
<IconButton className="absolute bottom-1 right-1" icon={ImageIcon} onClick={onClickIconButton} />
<div
className={clsx("h-full rounded-xl bg-gray-200 bg-cover bg-center bg-no-repeat")}
style={{
backgroundImage: select.data ? `url("${value}")` : "none",
}}
/>

<div
className={clsx("h-full rounded-xl bg-gray-200 bg-cover bg-center bg-no-repeat")}
style={{
backgroundImage: `url("${select.data ?? value}")`,
}}
/>
<input
{...field}
ref={internalRef}
accept="image/png, image/jpeg"
className="hidden"
type="file"
onChange={(event) => {
onChangeInputImage(event, onChange);
}}
/>
</div>
)}
rules={{ required: "Recipe picture is required" }}
/>
);
},
);

<input
{...field}
ref={ref}
accept="image/png, image/jpeg"
className="hidden"
type="file"
// value={value?.[name]}
onChange={(event) => {
const [file] = event.target.files ?? [];
if (file) {
select.mutate(file, {
onSuccess: (objectUrl) => {
onChange(objectUrl);
},
});
}
}}
/>
</div>
)}
rules={{ required: "Recipe picture is required" }}
/>
);
};
ImageUpload.displayName = "ImageUpload";
10 changes: 8 additions & 2 deletions packages/interface/src/components/SingleRoundHome.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Link from "next/link";
import { useAccount } from "wagmi";

import ConnectButton from "~/components/ConnectButton";
Expand Down Expand Up @@ -28,8 +29,13 @@ export const SingleRoundHome = ({ round }: ISingleRoundHomeProps): JSX.Element =

<p className="text-gray-400">{round.description}</p>

<Button size="auto" variant={roundState === ERoundState.RESULTS ? "tertiary" : "secondary"}>
<a href={`/rounds/${round.pollId}`}>View Projects</a>
<Button
as={Link}
href={`/rounds/${round.pollId}`}
size="auto"
variant={roundState === ERoundState.RESULTS ? "tertiary" : "secondary"}
>
View Projects
</Button>

{roundState === ERoundState.RESULTS && (
Expand Down
31 changes: 21 additions & 10 deletions packages/interface/src/components/SortFilter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,42 @@ import type { OrderBy, SortOrder } from "~/features/filter/types";
import { SortByDropdown } from "./SortByDropdown";
import { SearchInput } from "./ui/Form";

export const SortFilter = (): JSX.Element => {
export interface ISortFilterProps {
onSearchChange: (search: string) => void;
}

export const SortFilter = ({ onSearchChange }: ISortFilterProps): JSX.Element => {
const { orderBy, sortOrder, setFilter } = useFilter();

const [search, setSearch] = useState("");
useDebounce(() => setFilter({ search }), 500, [search]);

const onChange = useCallback(
const onChangeSearchInput = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
const term = e.target.value;
setSearch(term);
onSearchChange(term);
},
[setSearch, onSearchChange],
);

const onChangeSortByDropdown = useCallback(
async (sort: string) => {
const [order, sorting] = sort.split("_") as [OrderBy, SortOrder];

await setFilter({ orderBy: order, sortOrder: sorting }).catch();
},
[setSearch],
[setFilter],
);

return (
<div className="mb-2 flex flex-1 gap-2">
<SearchInput className="w-full" placeholder="Search project..." value={search} onChange={onChange} />
<SearchInput className="w-full" placeholder="Search project..." value={search} onChange={onChangeSearchInput} />

<SortByDropdown
options={["name_asc", "name_desc"]}
value={`${orderBy}_${sortOrder}`}
onChange={async (sort) => {
const [order, sorting] = sort.split("_") as [OrderBy, SortOrder];

await setFilter({ orderBy: order, sortOrder: sorting }).catch();
}}
onChange={onChangeSortByDropdown}
/>
</div>
);
Expand Down
13 changes: 10 additions & 3 deletions packages/interface/src/features/rounds/components/Projects.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import clsx from "clsx";
import Link from "next/link";
import { useCallback, useMemo } from "react";
import { useCallback, useDeferredValue, useMemo, useState } from "react";
import { FiAlertCircle } from "react-icons/fi";
import { Hex, zeroAddress } from "viem";

Expand All @@ -25,12 +25,19 @@ export interface IProjectsProps {
}

export const Projects = ({ pollId = "" }: IProjectsProps): JSX.Element => {
const [searchTerm, setSearchTerm] = useState("");
const deferredSearchTerm = useDeferredValue(searchTerm);

const roundState = useRoundState({ pollId });

const { getRoundByPollId } = useRound();
const round = useMemo(() => getRoundByPollId(pollId), [pollId, getRoundByPollId]);

const projects = useSearchProjects({ pollId, search: "", registryAddress: round?.registryAddress ?? zeroAddress });
const projects = useSearchProjects({
pollId,
search: deferredSearchTerm,
registryAddress: round?.registryAddress ?? zeroAddress,
});

const { isRegistered } = useMaci();
const { addToBallot, removeFromBallot, ballotContains, getBallot } = useBallot();
Expand Down Expand Up @@ -114,7 +121,7 @@ export const Projects = ({ pollId = "" }: IProjectsProps): JSX.Element => {
</Heading>

<div>
<SortFilter />
<SortFilter onSearchChange={setSearchTerm} />
</div>
</div>

Expand Down
14 changes: 10 additions & 4 deletions packages/interface/src/layouts/BaseLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import clsx from "clsx";
import Head from "next/head";
import { useRouter } from "next/router";
import { useTheme } from "next-themes";
import { type PropsWithChildren, createContext, useContext, useEffect, useCallback, useMemo } from "react";
import { type PropsWithChildren, createContext, useContext, useEffect, useCallback, useMemo, useState } from "react";
import { tv } from "tailwind-variants";
import { useAccount } from "wagmi";

Expand Down Expand Up @@ -54,7 +54,8 @@ export const BaseLayout = ({
type = undefined,
children = null,
}: IBaseLayoutProps): JSX.Element => {
const { theme } = useTheme();
const { theme, resolvedTheme } = useTheme();
const [clientTheme, setClientTheme] = useState("");
const router = useRouter();
const { address, isConnecting } = useAccount();

Expand All @@ -68,6 +69,11 @@ export const BaseLayout = ({
manageDisplay();
}, [manageDisplay]);

useEffect(() => {
// Ensure the theme is consistent on the client side
setClientTheme(theme || resolvedTheme || "");
}, [theme, resolvedTheme]);

const wrappedSidebar = <Sidebar side={sidebar}>{sidebarComponent}</Sidebar>;

const contextValue = useMemo(() => ({ eligibilityCheck, showBallot }), [eligibilityCheck, showBallot]);
Expand All @@ -79,7 +85,7 @@ export const BaseLayout = ({

<meta content={metadata.description} name="description" />

<link href="favicon.svg" rel="icon" />
<link href="/favicon.svg" rel="icon" />

<meta content={metadata.url} property="og:url" />

Expand All @@ -104,7 +110,7 @@ export const BaseLayout = ({
<meta content={metadata.image} name="twitter:image" />
</Head>

<div className={clsx("flex h-full min-h-screen flex-1 flex-col bg-white dark:bg-black", theme)}>
<div className={clsx("flex h-full min-h-screen flex-1 flex-col bg-white dark:bg-black", clientTheme)}>
{header}

<MainContainer type={type}>
Expand Down
2 changes: 1 addition & 1 deletion packages/interface/src/providers/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ function createWagmiConfig() {
const config = getDefaultConfig({
appName,
projectId,
ssr: false,
ssr: true,
chains: activeChains as unknown as readonly [Chain, ...Chain[]],
transports: {
[appConfig.config.network.id]: http(appConfig.getRPCURL()),
Expand Down
4 changes: 2 additions & 2 deletions packages/interface/src/server/api/routers/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { z } from "zod";

import { FilterSchema } from "~/features/filter/types";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { fetchApprovedProjects, fetchProjects } from "~/utils/fetchProjects";
import { fetchApprovedProjectsWithMetadata, fetchProjects } from "~/utils/fetchProjects";
import { getProjectCount } from "~/utils/registry";

import type { Chain, Hex } from "viem";
Expand Down Expand Up @@ -32,7 +32,7 @@ export const projectsRouter = createTRPCRouter({
.input(FilterSchema.extend({ search: z.string(), registryAddress: z.string() }))
.query(async ({ input }) =>
// get the projects that are approved
fetchApprovedProjects(input.registryAddress),
fetchApprovedProjectsWithMetadata(input.search, input.registryAddress),
),

projects: publicProcedure
Expand Down
Loading

0 comments on commit a9e5232

Please sign in to comment.