Skip to content

Commit

Permalink
fixes tags select flow and removes tag store
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotBraem committed Aug 6, 2024
1 parent 8387d1d commit 8bf452c
Show file tree
Hide file tree
Showing 8 changed files with 100 additions and 75 deletions.
6 changes: 2 additions & 4 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import "bootstrap-icons/font/bootstrap-icons.css";
import type { Metadata } from "next";
import { Manrope } from "next/font/google";
import "./globals.css";
import "bootstrap-icons/font/bootstrap-icons.css";

import Navbar from "@/components/ui/navbar";
import Footer from "@/components/ui/footer";
import TagsModal from "@/components/modals/tags";
import Navbar from "@/components/ui/navbar";
import site from "@/config/site";

const manrope = Manrope({ subsets: ["latin"] });
Expand Down Expand Up @@ -56,7 +55,6 @@ export default function RootLayout({ children }: RootLayoutProps) {
<div className="flex-grow">{children}</div>
<Footer />
</div>
<TagsModal />
</body>
</html>
);
Expand Down
2 changes: 2 additions & 0 deletions components/home/discover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Search from "@/components/search";
import FilteredProjects from "@/components/home/filtered-projects";
import { ProjectRecord } from "@/lib/types";
import { fetchAllProjects } from "@/lib/near-catalog";
import TagsModal from "@/components/modals/tags";

export default async function Discover() {
const projects = await fetchAllProjects();
Expand Down Expand Up @@ -41,6 +42,7 @@ export default async function Discover() {
<div className="z-1 relative my-16">
<Search tags={uniqueTags} />
<FilteredProjects projects={projects} />
<TagsModal tags={uniqueTags} />
</div>
</section>
);
Expand Down
20 changes: 12 additions & 8 deletions components/home/filtered-projects.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
"use client";

import { useState, useEffect, useCallback } from 'react';
import { useInView } from 'react-intersection-observer';
import { useState, useEffect, useCallback } from "react";
import { useInView } from "react-intersection-observer";
import { useSearchStore } from "@/store/search-store";
import { ProjectId, ProjectRecord } from "@/lib/types";
import ProjectsList from '@/components/ui/project-list';
import ProjectsList from "@/components/ui/project-list";

const ITEMS_PER_PAGE = 12;

interface ProjectsContainerProps {
projects: Record<ProjectId, ProjectRecord>;
}

export default function ProjectsContainer({ projects }: ProjectsContainerProps) {
export default function ProjectsContainer({
projects,
}: ProjectsContainerProps) {
const { tags, searchKey } = useSearchStore();
const [filteredProjects, setFilteredProjects] = useState<ProjectRecord[]>([]);
const [displayedProjects, setDisplayedProjects] = useState<ProjectRecord[]>([]);
const [displayedProjects, setDisplayedProjects] = useState<ProjectRecord[]>(
[],
);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const { ref, inView } = useInView();
Expand Down Expand Up @@ -60,11 +64,11 @@ export default function ProjectsContainer({ projects }: ProjectsContainerProps)

return (
<>
<ProjectsList
<ProjectsList
projects={displayedProjects}
allProjects={Object.values(projects)}
/>
{hasMore && <div ref={ref} style={{ height: '20px' }}></div>}
{hasMore && <div ref={ref} style={{ height: "20px" }}></div>}
</>
);
}
}
61 changes: 39 additions & 22 deletions components/modals/tags.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
"use client";

import * as Dialog from "@radix-ui/react-dialog";
import { useTagsModalStore } from "@/store/tags-modal-store";
import { useSearchStore } from "@/store/search-store";
import { useEffect, useState } from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { useState } from "react";

interface TagsModalProps {
tags: string[];
}

const Tags = ({
handleTagClick,
allTags,
selectedTags,
}: {
handleTagClick: (tag: string) => void;
allTags: string[];
selectedTags: string[];
}) => {
const { tags, allTags } = useSearchStore();
return (
<div className="flex flex-wrap gap-2 md:hidden">
{allTags.map((tag) => (
<div
key={tag}
onClick={() => handleTagClick(tag)}
className={`${tags.includes(tag) ? "" : "opacity-50"} inline-flex h-6 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full bg-[#17D9D466] px-2 py-1 text-xs font-medium text-white transition-colors duration-300 ease-in-out hover:bg-[#17D9D480] active:bg-[#17D9D499]`}
className={`${selectedTags.includes(tag) ? "" : "opacity-50"} inline-flex h-6 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full bg-[#17D9D466] px-2 py-1 text-xs font-medium text-white transition-colors duration-300 ease-in-out hover:bg-[#17D9D480] active:bg-[#17D9D499]`}
>
{tags.includes(tag) ? (
{selectedTags.includes(tag) ? (
<i className="bi bi-check text-xl" />
) : (
<i className="bi bi-x text-xl" />
Expand All @@ -31,31 +37,38 @@ const Tags = ({
);
};

function TagsModal() {
function TagsModal({ tags }: TagsModalProps) {
const { isOpen, setIsOpen } = useTagsModalStore();
const { tags, allTags, setTags } = useSearchStore();

const [allTagsEnabled, setAllTagsEnabled] = useState(true);
const [selectedTags, setSelectedTags] = useState<string[]>(tags);

useEffect(() => {
if (allTagsEnabled) {
setTags(allTags);
} else {
setTags([]);
}
}, [allTagsEnabled, setTags, allTags]);
const [allTagsEnabled, setAllTagsEnabled] = useState(true);

const handleTagClick = (tag: string) => {
let updatedTags: string[];
if (allTagsEnabled) {
setAllTagsEnabled(false);
setTags([tag]);
updatedTags = [tag];
} else {
if (tags.includes(tag)) {
setTags(tags.filter((searchTag) => searchTag !== tag));
if (selectedTags.includes(tag)) {
updatedTags = selectedTags.filter((t) => t !== tag);
} else {
setTags(Array.from(new Set([...tags, tag])));
updatedTags = [...selectedTags, tag];
}
}
setSelectedTags(updatedTags);
};

const handleToggleAllTags = () => {
setAllTagsEnabled((prev) => {
const newAllTagsEnabled = !prev;
if (newAllTagsEnabled) {
setSelectedTags(tags);
} else {
setSelectedTags([]);
}
return newAllTagsEnabled;
});
};

return (
Expand All @@ -77,7 +90,7 @@ function TagsModal() {
name="mobile-tags"
checked={allTagsEnabled}
className="peer sr-only"
onChange={() => setAllTagsEnabled((prev) => !prev)}
onChange={handleToggleAllTags}
aria-label="Toggle all tags"
/>
<div className="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"></div>
Expand All @@ -86,7 +99,11 @@ function TagsModal() {
</span>
</label>
<div className="flex flex-col gap-2">
<Tags handleTagClick={handleTagClick} />
<Tags
handleTagClick={handleTagClick}
allTags={tags}
selectedTags={selectedTags}
/>
</div>
</div>
<div className="mt-6 flex justify-center">
Expand Down
59 changes: 33 additions & 26 deletions components/search.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
"use client";

import { useSearchStore } from "@/store/search-store";
import { useEffect, useState } from "react";
import { useTagsModalStore } from "@/store/tags-modal-store";
import { useState } from "react";
import SearchInput from "./search-input";

const Tags = ({
handleTagClick,
allTags,
selectedTags,
}: {
handleTagClick: (tag: string) => void;
allTags: string[];
selectedTags: string[];
}) => {
const { tags, allTags } = useSearchStore();
return (
<div className="hidden flex-wrap gap-2 md:flex">
{allTags.map((tag) => (
{allTags.map((tag: string) => (
<div
key={tag}
onClick={() => handleTagClick(tag)}
className={`${tags.includes(tag) ? "" : "opacity-50"} inline-flex h-6 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full bg-[#17D9D466] px-2 py-1 text-xs font-medium text-white transition-colors duration-300 ease-in-out hover:bg-[#17D9D480] active:bg-[#17D9D499]`}
className={`${selectedTags.includes(tag) ? "" : "opacity-50"} inline-flex h-6 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-full bg-[#17D9D466] px-2 py-1 text-xs font-medium text-white transition-colors duration-300 ease-in-out hover:bg-[#17D9D480] active:bg-[#17D9D499]`}
>
{tag}
</div>
Expand All @@ -31,34 +33,35 @@ interface SearchProps {
}

export default function Search({ tags }: SearchProps) {
const { setTags, tags: searchTags, setAllTags, allTags } = useSearchStore();
const { isOpen, setIsOpen } = useTagsModalStore();
const [allTagsEnabled, setAllTagsEnabled] = useState(true);

useEffect(() => {
setAllTags(tags);
setTags(tags);
}, [setAllTags, setTags, tags]);

useEffect(() => {
if (allTagsEnabled) {
setTags(tags);
} else {
setTags([]);
}
}, [allTagsEnabled, tags, setTags]);
const [selectedTags, setSelectedTags] = useState<string[]>(tags);

const handleTagClick = (tag: string) => {
let updatedTags: string[];
if (allTagsEnabled) {
setAllTagsEnabled(false);
setTags([tag]);
updatedTags = [tag];
} else {
if (searchTags.includes(tag)) {
setTags(searchTags.filter((searchTag) => searchTag !== tag));
if (selectedTags.includes(tag)) {
updatedTags = selectedTags.filter((t) => t !== tag);
} else {
setTags(Array.from(new Set([...searchTags, tag])));
updatedTags = [...selectedTags, tag];
}
}
setSelectedTags(updatedTags);
};

const handleToggleAllTags = () => {
setAllTagsEnabled((prev) => {
const newAllTagsEnabled = !prev;
if (newAllTagsEnabled) {
setSelectedTags(tags);
} else {
setSelectedTags([]);
}
return newAllTagsEnabled;
});
};

return (
Expand All @@ -73,7 +76,7 @@ export default function Search({ tags }: SearchProps) {
checked={allTagsEnabled}
className="peer sr-only"
aria-label="Toggle all tags"
onChange={() => setAllTagsEnabled((prev) => !prev)}
onChange={handleToggleAllTags}
/>
<div className="peer relative h-6 w-11 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-5 after:w-5 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rtl:peer-checked:after:-translate-x-full dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800"></div>
<span className="ms-3 text-sm font-medium text-gray-300">All Tags</span>
Expand All @@ -83,12 +86,16 @@ export default function Search({ tags }: SearchProps) {
className="mt-4 flex w-full cursor-pointer items-center justify-between truncate rounded-full border border-[#3F3F3F] bg-[#1A1A17] px-4 py-2 text-white md:hidden"
>
<span>
{`Selected Tags: ${searchTags.length === allTags.length ? "All" : searchTags.join(", ")}`}
{`Selected Tags: ${selectedTags.length === tags.length ? "All" : selectedTags.join(", ")}`}
</span>
<i className="bi bi-chevron-down h-4 w-4 text-xl" />
</div>

<Tags handleTagClick={handleTagClick} />
<Tags
handleTagClick={handleTagClick}
allTags={tags}
selectedTags={selectedTags}
/>
</div>
);
}
7 changes: 5 additions & 2 deletions components/ui/project-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ interface ProjectsListProps {
allProjects: ProjectRecord[];
}

export default function ProjectsList({ projects, allProjects }: ProjectsListProps) {
export default function ProjectsList({
projects,
allProjects,
}: ProjectsListProps) {
if (projects.length === 0) {
return (
<div className="my-32 flex flex-col items-center justify-center gap-4 font-medium text-[#BEBDBE]">
Expand Down Expand Up @@ -38,4 +41,4 @@ export default function ProjectsList({ projects, allProjects }: ProjectsListProp
</div>
</>
);
}
}
12 changes: 7 additions & 5 deletions playwright-tests/tests/home.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,20 +90,22 @@ test.describe("Homepage", () => {
).toBeVisible();
});

test("should deselect and reselect all tags when toggling", async ({ page }) => {
test("should deselect and reselect all tags when toggling", async ({
page,
}) => {
const tagsToggle = page.getByLabel("Toggle all tags");

await expect(tagsToggle).toBeVisible();

const tags = await page.getByTestId("tag").all();

// default should be all tags checked
await expect(tagsToggle).toBeChecked();
for (const tag of tags) {
// verify that all tags do not have classname opacity-50
expect(tag).not.toHaveClass(/opacity-50/);
}

// toggle all tags off
await tagsToggle.setChecked(true);
await page.waitForTimeout(100);
Expand Down
8 changes: 0 additions & 8 deletions store/search-store.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { create } from "zustand";

interface SearchStore {
tags: string[];
setTags: (tags: string[]) => void;
allTags: string[];
setAllTags: (allTags: string[]) => void;
searchKey: string;
setSearchKey: (searchKey: string) => void;
}

export const useSearchStore = create<SearchStore>((set) => ({
tags: [],
setTags: (tags) => set({ tags }),
allTags: [],
setAllTags: (allTags) => set({ allTags }),
searchKey: "",
setSearchKey: (searchKey) => set({ searchKey }),
}));

0 comments on commit 8bf452c

Please sign in to comment.