From 2e363af69365b8bc5b03f6a30819b3150bf05629 Mon Sep 17 00:00:00 2001 From: Zafeeruddin Date: Sun, 6 Oct 2024 01:24:57 +0530 Subject: [PATCH] Added question labels --- package.json | 3 +- pnpm-lock.yaml | 92 +++++++++++++++- src/actions/types.ts | 1 + src/app/(main)/(pages)/question/page.tsx | 4 + src/components/LabelDropdown.tsx | 127 +++++++++++++++++++++++ src/components/posts/PostCard.tsx | 33 +++++- src/components/questions/ClearFIlter.tsx | 22 ++++ 7 files changed, 275 insertions(+), 7 deletions(-) create mode 100644 src/components/LabelDropdown.tsx create mode 100644 src/components/questions/ClearFIlter.tsx diff --git a/package.json b/package.json index f26aa8e56..ffcadfa97 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,9 @@ "@prisma/client": "^5.18.0", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-navigation-menu": "^1.1.4", "@radix-ui/react-popover": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24f477ce4..7cfdb5436 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: specifier: ^9.4.0 version: 9.7.0(react@18.3.1) '@prisma/client': - specifier: ^5.6.0 + specifier: ^5.18.0 version: 5.18.0(prisma@5.18.0) '@radix-ui/react-accordion': specifier: ^1.1.2 @@ -32,11 +32,14 @@ importers: '@radix-ui/react-avatar': specifier: ^1.0.4 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.0.5 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dropdown-menu': - specifier: ^2.0.6 + specifier: ^2.1.1 version: 2.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-label': specifier: ^2.0.2 @@ -47,6 +50,9 @@ importers: '@radix-ui/react-popover': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.2.0 + version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -54,7 +60,7 @@ importers: specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': - specifier: ^1.0.2 + specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-switch': specifier: ^1.1.0 @@ -62,6 +68,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tabler/icons-react': + specifier: ^3.14.0 + version: 3.17.0(react@18.3.1) '@types/bcrypt': specifier: ^5.0.2 version: 5.0.2 @@ -289,7 +298,7 @@ importers: specifier: ^0.6.1 version: 0.6.6(prettier@3.3.3) prisma: - specifier: ^5.17.0 + specifier: ^5.18.0 version: 5.18.0 tailwindcss: specifier: ^3.3.0 @@ -1631,6 +1640,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.1.1': + resolution: {integrity: sha512-0i/EKJ222Afa1FE0C6pNJxDq1itzcl3HChE9DwskA4th4KRse8ojx8a1nVcOjwJdbpDLcz7uol77yYnQNMHdKw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collapsible@1.1.0': resolution: {integrity: sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==} peerDependencies: @@ -1858,6 +1880,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-radio-group@1.2.0': + resolution: {integrity: sha512-yv+oiLaicYMBpqgfpSPw6q+RyXlLdIpQWDHZbUKURxe+nEh53hFXPPlfhfQQtYkS5MMK/5IWIa76SksleQZSzw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.0': resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: @@ -2331,6 +2366,14 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} + '@tabler/icons-react@3.17.0': + resolution: {integrity: sha512-Ndm9Htv7KpIU1PYYrzs5EMhyA3aZGcgaxUp9Q1XOxcRZ+I0X+Ub2WS5f4bkRyDdL1s0++k2T9XRgmg2pG113sw==} + peerDependencies: + react: '>= 16' + + '@tabler/icons@3.17.0': + resolution: {integrity: sha512-sCSfAQ0w93KSnSL7tS08n73CdIKpuHP8foeLMWgDKiZaCs8ZE//N3ytazCk651ZtruTtByI3b+ZDj7nRf+hHvA==} + '@testing-library/dom@10.1.0': resolution: {integrity: sha512-wdsYKy5zupPyLCW2Je5DLHSxSfbIp6h80WoHOQc+RPtmPGA52O9x5MJEkv92Sjonpq+poOAtUKhh1kBGAXBrNA==} engines: {node: '>=18'} @@ -8685,6 +8728,22 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-checkbox@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-collapsible@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -8932,6 +8991,24 @@ snapshots: '@types/react': 18.3.3 '@types/react-dom': 18.3.0 + '@radix-ui/react-radio-group@1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-context': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-presence': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@18.3.3)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.3.3)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -9580,6 +9657,13 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@tabler/icons-react@3.17.0(react@18.3.1)': + dependencies: + '@tabler/icons': 3.17.0 + react: 18.3.1 + + '@tabler/icons@3.17.0': {} + '@testing-library/dom@10.1.0': dependencies: '@babel/code-frame': 7.24.7 diff --git a/src/actions/types.ts b/src/actions/types.ts index ac252c469..6c87fc691 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -13,6 +13,7 @@ export interface QueryParams { timestamp?: number; editCommentId?: number; newPost?: 'open' | 'close'; + tags?: string; } export enum TabType { md = 'Most downvotes', diff --git a/src/app/(main)/(pages)/question/page.tsx b/src/app/(main)/(pages)/question/page.tsx index e050f3a00..2544d9713 100644 --- a/src/app/(main)/(pages)/question/page.tsx +++ b/src/app/(main)/(pages)/question/page.tsx @@ -24,6 +24,8 @@ import { authOptions } from '@/lib/auth'; import PostCard from '@/components/posts/PostCard'; import Pagination from '@/components/Pagination'; import { redirect } from 'next/navigation'; +import LabelDropdown from '@/components/LabelDropdown'; +import { ClearFilter } from '@/components/questions/ClearFIlter'; type QuestionsResponse = { data: ExtendedQuestion[] | null; @@ -155,6 +157,7 @@ export default async function QuestionsPage({
+
+ {/* Chat */}
{response?.data?.map((post) => ( diff --git a/src/components/LabelDropdown.tsx b/src/components/LabelDropdown.tsx new file mode 100644 index 000000000..61e70b40e --- /dev/null +++ b/src/components/LabelDropdown.tsx @@ -0,0 +1,127 @@ +'use client'; + +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, Search, Tags } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useSearchParams, useRouter } from 'next/navigation'; +import { getUpdatedUrl } from '@/lib/utils'; +import { Button } from './ui/button'; +import { ExtendedQuestion } from '@/actions/question/types'; +import Tag from './posts/tag'; + +export default function LabelDropdown({posts}:{posts:ExtendedQuestion[] | null}) { + const [selectedLabels, setSelectedLabels] = useState([]); + const [searchTerm, setSearchTerm] = useState(''); + const [isOpen, setIsOpen] = useState(false); + const [labels,setLabels] = useState([]); + const searchParams = useSearchParams(); + const router = useRouter(); + + useEffect(() => { + const taggedPosts:ExtendedQuestion[]=[]; + const filterPosts = () => { + if (posts!=null) { + posts.filter(post => { + if (post.tags.length!=0) taggedPosts.push(post); +}); + } + }; + filterPosts(); + + const newLabels:string[]=[]; + taggedPosts.map(post => { + const tags = post.tags; + tags.map(tag => { + tag= tag.trim(); + const checkTag = tag === tag.toUpperCase() ? tag : tag[0].toUpperCase() + tag.slice(1); + if (!newLabels.includes(checkTag) && checkTag.length!=0) { + newLabels.push(checkTag); + } + }); + }); + + setLabels(newLabels.sort()); + },[posts]); + + const handleToggleLabel = (id: string) => { + setSelectedLabels((prev) => + prev.includes(id) ? prev.filter((label) => label !== id) : [...prev, id] + ); + }; + + const filteredLabels = labels.filter((label) => + label.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const updatedUrl = getUpdatedUrl('/question', Object.fromEntries(searchParams.entries()), { + tags: selectedLabels.join(','), + }); + + return ( + + + + + + + +
+ setSearchTerm(e.target.value)} + /> + +
+ + {filteredLabels.length > 0 ? ( +
+ {filteredLabels.map((label,index) => ( + { + e.preventDefault(); + handleToggleLabel(label); + }} + > + + ))} +
+ ) : ( +
No labels found
+ )} +
+
+
+ ); +} + +const Label = ({name}:{name:string}) => { + return ( + <> + { name!=="" && + + } + + ); +}; \ No newline at end of file diff --git a/src/components/posts/PostCard.tsx b/src/components/posts/PostCard.tsx index 3b35f0ec0..ef26e538e 100644 --- a/src/components/posts/PostCard.tsx +++ b/src/components/posts/PostCard.tsx @@ -1,7 +1,7 @@ 'use client'; import '@uiw/react-md-editor/markdown-editor.css'; import '@uiw/react-markdown-preview/markdown.css'; -import React, { useState, useTransition } from 'react'; +import React, { useEffect, useState, useTransition } from 'react'; import VoteForm from './form/form-vote'; import dayjs from 'dayjs'; import relativeTime from 'dayjs/plugin/relativeTime'; @@ -25,7 +25,7 @@ import { Avatar, AvatarFallback } from '../ui/avatar'; import { Reply } from 'lucide-react'; import { ROLES } from '@/actions/types'; import { FormPostErrors } from './form/form-errors'; -import { useRouter } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { Button } from '../ui/button'; interface IProps { @@ -51,6 +51,7 @@ const PostCard: React.FC = ({ isAnswer = true, parentAuthorName, }) => { + const [showQuestion,setShowQuestion]= useState(true); const { theme } = useTheme(); const [markDownValue, setMarkDownValue] = useState(''); const [enableReply, setEnableReply] = useState(false); @@ -60,6 +61,31 @@ const PostCard: React.FC = ({ } }; + const searchParams = useSearchParams(); + const tags = searchParams.get("tags"); + const newTags =tags?.split(","); + + useEffect(() => { + console.log("chekc new tags",newTags); + if (newTags==undefined || !isExtendedQuestion(post)) { + return; + } + const tagExists = post.tags?.some(tag => { + const trimmedTag = tag.trim(); // Trim first + const formattedTag = trimmedTag === trimmedTag.toUpperCase() + ? trimmedTag + : trimmedTag[0].toUpperCase() + trimmedTag.slice(1); + + return newTags.includes(formattedTag); + }); + + if (isExtendedQuestion(post) && tagExists) { + setShowQuestion(true); + } else { + setShowQuestion(false); + } + },[searchParams]); + const router = useRouter(); const [isPending, startTransition] = useTransition(); @@ -94,6 +120,7 @@ const PostCard: React.FC = ({ }; return ( + <> {showQuestion &&
= ({
)}
+} + ); }; diff --git a/src/components/questions/ClearFIlter.tsx b/src/components/questions/ClearFIlter.tsx new file mode 100644 index 000000000..7f720c958 --- /dev/null +++ b/src/components/questions/ClearFIlter.tsx @@ -0,0 +1,22 @@ +"use client"; +import { useSearchParams, useRouter } from 'next/navigation'; +import { Button } from '@/components/ui/button'; +import { X } from 'lucide-react'; + +export const ClearFilter = () => { + const searchParams = useSearchParams(); + const hasTags = searchParams.get('tags'); + + return ( + hasTags && ( +
+ +
+ ) + ); +};