diff --git a/locales/en.json b/locales/en.json index 9596a3a..972b5de 100644 --- a/locales/en.json +++ b/locales/en.json @@ -146,6 +146,7 @@ "no-user-profile": "There are no users with this username registered in CapX", "no-report-rights": "You either do not have permission to update this report or this report does not exists!", "body-profile-languages-title": "Languages", + "body-profile-languages-alt-icon": "Languages icon", "body-profile-known-capacities-title": "Known capacities", "body-profile-available-capacities-title": "Available capacities", "body-profile-wanted-capacities-title": "Wanted capacities", @@ -160,7 +161,7 @@ "edit-profile-image-title": "Image profile", "edit-profile-choose-avatar": "Choose avatar", "edit-profile-use-wikidata": "Use Wikidata item", - "edit-profile-consent-wikidata": "I consent displaying my Wikidata item image on CapX profile (if existent).", + "edit-profile-consent-wikidata": "By clicking here, I agree to have my Wikidata item image on CapX's profile. The tool will look in Wikidata for an image related to your username and will show it if found.", "edit-profile-save": "Save profile", "edit-profile-cancel": "Cancel edit", "edit-profile-mini-bio": "Mini bio", @@ -195,11 +196,10 @@ "edit-profile-territory": "Inform your geographic location by region or country.", "edit-profile-display-links": "Let the community know what your organization is working on. Share up to four Wikimedia links and related images on Commons.", "edit-profile-wikidata-item": "Wikidata Item", - "edit-profile-consent-wikidata-item": "I consent displaying my Wikidata item on CapX profile (if existent).", + "edit-profile-consent-wikidata-item": "By clicking here, I agree to have my Wikidata item displayed on CapX's profile. The tool will look in Wikidata for a QID related to your username and will show it if found.", "edit-profile-wikimedia-projects": "Inform the Wikimedia Projects you have interest in.", "edit-profile-insert-project": "Insert project", "edit-profile-insert-link": "Insert document URL", - "privacy-policy-icon": "Icon privacy policy", "privacy-policy-title": "Capacity Exchange Privacy Policy", "privacy-policy-capacity-exchange-text": "This site is an international project currently coordinated by $1. It's running on Wikimedia Cloud Services, specifically at $2, and therefore is subject to the $3, making limited use of Private Information. All content is CC-BY-SA. Code contributions welcome.", @@ -215,5 +215,29 @@ "privacy-policy-private-information-text01": "If you wish, you can remove information about yourself by clickingon the red bottom \"Delete Profile\" on the user's profile editing page.", "privacy-policy-private-information-text02": "If you are a representative of an organization and have the rights to edit its profile, you can clean its data by editing the organization's profile and leaving all blank. You can remove yourself from the role or representative and, once there is no on linked to the organization, the profile will not be displayed on the list of organizations.", "privacy-policy-private-information-text03": "Data use and retention is governed by the $1. This tool is in compliance with the GPDR.", - "privacy-policy-private-information-text03-link":"Wikimedia Cloud Service Terms of use" + "privacy-policy-private-information-text03-link":"Wikimedia Cloud Service Terms of use", + "filters-learners": "Learners", + "filters-sharers": "Sharers", + "filters-search-by-capacities": "Search by capacities", + "filters-capacities": "Capacities", + "filters-capacities-alt-icon": "Capacities icon", + "filters-all-profiles": "All profiles", + "filters-all-profiles-alt-icon": "All profiles icon", + "filters-organization-profile": "Organization profile", + "filters-organization-profile-alt-icon": "Organization profile icon", + "filters-user-profile": "User profile", + "filters-user-profile-alt-icon": "User profile icon", + "filters-show-results": "Show results", + "filters-clear-all": "Clear all", + "filters-exchange-with-alt-icon": "Exchange with icon", + "filters-search-icon": "Search icon", + "filters-back-icon": "Back icon", + "filters-profiles": "Profiles", + "filters-remove-item-alt-icon": "Remove item icon", + "filters-select-alt-icon": "Select icon", + "filters-territory-title": "Territories", + "filters-add-territory": "Add territory", + "filters-exchange-with": "Exchange with", + "filters-title": "Filters", + "filters-icon": "Filters icon" } diff --git a/public/static/images/all_profiles_icon.svg b/public/static/images/all_profiles_icon.svg new file mode 100644 index 0000000..73fc582 --- /dev/null +++ b/public/static/images/all_profiles_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/static/images/all_profiles_icon_white.svg b/public/static/images/all_profiles_icon_white.svg new file mode 100644 index 0000000..8ba7074 --- /dev/null +++ b/public/static/images/all_profiles_icon_white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/static/images/arrow_back_icon.svg b/public/static/images/arrow_back_icon.svg new file mode 100644 index 0000000..a3c97a3 --- /dev/null +++ b/public/static/images/arrow_back_icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/static/images/arrow_back_icon_white.svg b/public/static/images/arrow_back_icon_white.svg new file mode 100644 index 0000000..9a3d512 --- /dev/null +++ b/public/static/images/arrow_back_icon_white.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/static/images/capx_icon.svg b/public/static/images/capx_icon.svg new file mode 100644 index 0000000..0e9ea17 --- /dev/null +++ b/public/static/images/capx_icon.svg @@ -0,0 +1,12 @@ + + + diff --git a/public/static/images/capx_icon_white.svg b/public/static/images/capx_icon_white.svg new file mode 100644 index 0000000..a0a977f --- /dev/null +++ b/public/static/images/capx_icon_white.svg @@ -0,0 +1,12 @@ + + + diff --git a/public/static/images/filter_icon.svg b/public/static/images/filter_icon.svg new file mode 100644 index 0000000..0ba9ebe --- /dev/null +++ b/public/static/images/filter_icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/static/images/filter_icon_white.svg b/public/static/images/filter_icon_white.svg new file mode 100644 index 0000000..76e6095 --- /dev/null +++ b/public/static/images/filter_icon_white.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/static/images/learner_icon.svg b/public/static/images/learner_icon.svg new file mode 100644 index 0000000..41a1a73 --- /dev/null +++ b/public/static/images/learner_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/static/images/learner_icon_white.svg b/public/static/images/learner_icon_white.svg new file mode 100644 index 0000000..d339a78 --- /dev/null +++ b/public/static/images/learner_icon_white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/static/images/profiles_icon.svg b/public/static/images/profiles_icon.svg new file mode 100644 index 0000000..8205e95 --- /dev/null +++ b/public/static/images/profiles_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/static/images/profiles_icon_white.svg b/public/static/images/profiles_icon_white.svg new file mode 100644 index 0000000..dc6054e --- /dev/null +++ b/public/static/images/profiles_icon_white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/static/images/search_icon.svg b/public/static/images/search_icon.svg new file mode 100644 index 0000000..110221d --- /dev/null +++ b/public/static/images/search_icon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/static/images/search_icon_white.svg b/public/static/images/search_icon_white.svg new file mode 100644 index 0000000..115f07c --- /dev/null +++ b/public/static/images/search_icon_white.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/static/images/sharer_icon.svg b/public/static/images/sharer_icon.svg new file mode 100644 index 0000000..12f0586 --- /dev/null +++ b/public/static/images/sharer_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/static/images/sharer_icon_white.svg b/public/static/images/sharer_icon_white.svg new file mode 100644 index 0000000..031ad41 --- /dev/null +++ b/public/static/images/sharer_icon_white.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/__tests__/components/CapacityFeedCard.test.tsx b/src/__tests__/components/CapacityFeedCard.test.tsx index 6e98cb9..78fbf3b 100644 --- a/src/__tests__/components/CapacityFeedCard.test.tsx +++ b/src/__tests__/components/CapacityFeedCard.test.tsx @@ -1,6 +1,17 @@ import { render, screen } from '@testing-library/react'; import { ProfileCard } from '@/app/(auth)/feed/components/Card'; -import { ProfileType } from '@/app/(auth)/feed/page'; +import { ProfileCapacityType } from '@/app/(auth)/feed/page'; + +// Mock the router +jest.mock('next/navigation', () => ({ + useRouter: () => ({ + push: jest.fn(), + replace: jest.fn(), + prefetch: jest.fn() + }), + usePathname: () => '/feed', + useSearchParams: () => new URLSearchParams() +})); // Mock the next/image component jest.mock('next/image', () => ({ @@ -34,7 +45,9 @@ const mockPageContent = { "body-profile-available-capacities-title": "Available capacities", "body-profile-wanted-capacities-title": "Wanted capacities", "body-profile-languages-title": "Languages", - "body-profile-section-title-territory": "Territory" + "body-profile-section-title-territory": "Territory", + "body-profile-languages-alt-icon": "Languages icon", + "filters-search-by-capacities": "Search by capacities", }; jest.mock('@/contexts/AppContext', () => ({ @@ -52,7 +65,7 @@ describe('ProfileCard', () => { describe('Learner Profile', () => { const learnerProps = { ...defaultProps, - type: ProfileType.Learner, + type: ProfileCapacityType.Learner, capacities: ['Coding', 'Design'], languages: ['English', 'Portuguese'], territory: 'Brazil' @@ -94,7 +107,7 @@ describe('ProfileCard', () => { describe('Sharer Profile', () => { const sharerProps = { ...defaultProps, - type: ProfileType.Sharer, + type: ProfileCapacityType.Sharer, capacities: ['Teaching', 'Mentoring'], languages: ['Spanish', 'French'], territory: 'Argentina' @@ -134,7 +147,7 @@ describe('ProfileCard', () => { describe('Empty States', () => { it('should display no-data message when arrays are empty', () => { - render(); + render(); const noDataMessages = screen.getAllByText("You haven't filled this field yet"); expect(noDataMessages).toHaveLength(3); // One for each empty section @@ -143,7 +156,7 @@ describe('ProfileCard', () => { describe('Layout Structure', () => { it('should have a two-column layout on desktop', () => { - render(); + render(); const container = screen.getByRole('article'); expect(container).toHaveClass('md:grid-cols-[350px_1fr]'); diff --git a/src/app/(auth)/feed/components/Card.tsx b/src/app/(auth)/feed/components/Card.tsx index 82d80c0..c09ef71 100644 --- a/src/app/(auth)/feed/components/Card.tsx +++ b/src/app/(auth)/feed/components/Card.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Image from 'next/image'; import { useTheme } from '@/contexts/ThemeContext'; import { useApp } from '@/contexts/AppContext'; -import { ProfileType } from '../page'; +import { ProfileCapacityType } from '../page'; import LanguageIcon from "@/public/static/images/language.svg"; import LanguageIconWhite from "@/public/static/images/language_white.svg"; @@ -17,10 +17,11 @@ import TerritoryIconWhite from "@/public/static/images/territory_white.svg"; import AccountCircle from "@/public/static/images/account_circle.svg"; import AccountCircleWhite from "@/public/static/images/account_circle_white.svg"; import { ProfileItem } from '@/components/ProfileItem'; +import { useRouter } from 'next/navigation'; interface ProfileCardProps { username: string; - type: ProfileType; + type: ProfileCapacityType; capacities: (number | string)[]; languages?: string[]; territory?: string; @@ -30,7 +31,7 @@ interface ProfileCardProps { export const ProfileCard = ({ username, - type = ProfileType.Learner, + type = ProfileCapacityType.Learner, capacities = [], languages = [], territory = '', @@ -38,6 +39,7 @@ export const ProfileCard = ({ }: ProfileCardProps) => { const { darkMode } = useTheme(); const { pageContent } = useApp(); + const router = useRouter(); const capacitiesTitle = type === 'learner' ? pageContent["body-profile-wanted-capacities-title"] : pageContent["body-profile-available-capacities-title"]; const wantedCapacitiesIcon = darkMode ? TargetIconWhite : TargetIcon; @@ -53,7 +55,7 @@ export const ProfileCard = ({ ? "text-purple-200 border-purple-200" : "text-[#05A300] border-[#05A300]"; - + const formattedUsername = username.replace(' ', '_'); return (
{ + router.push(`/profile/${formattedUsername}`); + }} > {pageContent['body-profile-languages-title']} void; +} + +export function CheckboxButton({ + icon, + iconDark, + label, + checked, + onClick +}: CheckboxButtonProps) { + const { darkMode } = useTheme(); + + return ( + + ); +} diff --git a/src/app/(auth)/feed/components/Filters.tsx b/src/app/(auth)/feed/components/Filters.tsx new file mode 100644 index 0000000..a145cad --- /dev/null +++ b/src/app/(auth)/feed/components/Filters.tsx @@ -0,0 +1,419 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import { useApp } from '@/contexts/AppContext'; +import { useTheme } from '@/contexts/ThemeContext'; +import Image from 'next/image'; +import { FilterState, ProfileCapacityType, ProfileFilterType } from '../page'; + +import ArrowBackIcon from '@/public/static/images/arrow_back_icon.svg'; +import ArrowBackIconWhite from '@/public/static/images/arrow_back_icon_white.svg'; +import CapxIcon from '@/public/static/images/capx_icon.svg'; +import CapxIconWhite from '@/public/static/images/capx_icon_white.svg'; +import ProfilesIcon from '@/public/static/images/profiles_icon.svg'; +import ProfilesIconWhite from '@/public/static/images/profiles_icon_white.svg'; +import CloseIcon from "@/public/static/images/close_mobile_menu_icon_light_mode.svg"; +import CloseIconWhite from "@/public/static/images/close_mobile_menu_icon_dark_mode.svg"; +import BaseButton from '@/components/BaseButton'; +import SearchIcon from "@/public/static/images/search_icon.svg"; +import SearchIconWhite from "@/public/static/images/search_icon_white.svg"; +import AccountCircleIcon from "@/public/static/images/account_circle.svg"; +import AccountCircleIconWhite from "@/public/static/images/account_circle_white.svg"; +import OrganizationIcon from "@/public/static/images/supervised_user_circle.svg"; +import OrganizationIconWhite from "@/public/static/images/supervised_user_circle_white.svg"; +import AllProfilesIcon from "@/public/static/images/all_profiles_icon.svg"; +import AllProfilesIconWhite from "@/public/static/images/all_profiles_icon_white.svg"; +import LearnerIcon from "@/public/static/images/learner_icon.svg"; +import LearnerIconWhite from "@/public/static/images/learner_icon_white.svg"; +import SharerIcon from "@/public/static/images/sharer_icon.svg"; +import SharerIconWhite from "@/public/static/images/sharer_icon_white.svg"; + +import { useTerritories } from "@/hooks/useTerritories"; +import { useLanguage } from '@/hooks/useLanguage'; +import { useSession } from 'next-auth/react'; +import { LanguageSelector } from './LanguageSelector'; +import { CheckboxButton } from './CheckboxButton'; +import { TerritorySelector } from './TerritorySelector'; + +interface FiltersProps { + onClose: () => void; + onApplyFilters: (filters: any) => void; + initialFilters: FilterState +} + +export function Filters({ onClose, onApplyFilters, initialFilters }: FiltersProps) { + const { darkMode } = useTheme(); + const { pageContent } = useApp(); + const { data: session } = useSession(); + const token = session?.user?.token; + const { languages } = useLanguage(token); + const { territories } = useTerritories(token); + const [searchCapacity, setSearchCapacity] = useState(''); + const [filters, setFilters] = useState(initialFilters); + + const handleAddCapacity = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchCapacity.trim()) { + const trimmedCapacity = searchCapacity.trim(); + if (!filters.capacities.includes(trimmedCapacity)) { + setFilters(prev => ({ + ...prev, + capacities: [...prev.capacities, trimmedCapacity] + })); + } + setSearchCapacity(''); + } + }; + + const handleRemoveCapacity = (capacity: string) => { + setFilters(prev => ({ + ...prev, + capacities: prev.capacities.filter(cap => cap !== capacity) + })); + }; + + const handleApply = () => { + onApplyFilters(filters); + }; + + const handleClearAll = () => { + setFilters({ + capacities: [], + profileCapacityTypes: [], + territories: [], + languages: [], + profileFilter: ProfileFilterType.Both + }); + setSearchCapacity(''); + }; + + const handleProfileFilterChange = (type: ProfileFilterType) => { + setFilters(prev => ({ + ...prev, + profileFilter: type + })); + }; + + const handleProfileCapacityTypeToggle = (type: ProfileCapacityType) => { + setFilters(prev => { + const types = prev.profileCapacityTypes.includes(type) + ? prev.profileCapacityTypes.filter(t => t !== type) + : [...prev.profileCapacityTypes, type]; + return { ...prev, profileCapacityTypes: types }; + }); + }; + + // Avoid multiple scrolls when the modal is open + useEffect(() => { + document.body.style.overflow = 'hidden'; + + // Cleanup: restore scroll when the modal closes + return () => { + document.body.style.overflow = 'auto'; + }; + }, []); + + return ( +
+ {/* Dark Overlay */} +
+ + {/* Cotainer's Modal */} +
+ {/* Header */} +
+
+ +

+ {pageContent["filters-title"]} +

+
+
+ + {/* Content */} +
+
+ {/* Capacities */} +
+
+ {pageContent["filters-capacities-alt-icon"]} +

+ {pageContent["filters-capacities"]} +

+
+
+ setSearchCapacity(e.target.value)} + onKeyDown={handleAddCapacity} + placeholder={pageContent["filters-search-by-capacities"]} + className={` + w-full p-2 rounded-lg border + ${darkMode + ? 'bg-capx-dark-box-bg text-white border-gray-700 placeholder-gray-400' + : 'bg-white border-gray-300 placeholder-gray-500' + } + `} + /> +
+ {pageContent["filters-search-icon"]} +
+
+ + {/* Selected Capacities */} + {filters.capacities.length > 0 && ( +
+ {filters.capacities.map((capacity, index) => ( +
+ {capacity} + +
+ ))} +
+ )} +
+ + {/* Divider */} +
+ + {/* Exchange with */} +
+
+ {pageContent["filters-exchange-with-alt-icon"]} +

+ {pageContent["filters-exchange-with"]} +

+
+
+ handleProfileCapacityTypeToggle(ProfileCapacityType.Learner)} + /> + handleProfileCapacityTypeToggle(ProfileCapacityType.Sharer)} + /> +
+
+ + {/* Divider */} +
+ + {/* Territory */} + { + setFilters(prev => ({ + ...prev, + territories: prev.territories.includes(territoryId) + ? prev.territories.filter(id => id !== territoryId) + : [...prev.territories, territoryId] + })); + }} + placeholder={pageContent["filters-add-territory"]} + /> + + {/* Divider */} +
+ + {/* Languages */} + { + setFilters(prev => ({ + ...prev, + languages: prev.languages.includes(languageId) + ? prev.languages.filter(id => id !== languageId) + : [...prev.languages, languageId] + })); + }} + placeholder={pageContent["edit-profile-add-language"]} + /> + + {/* Divider */} +
+ + {/* Profiles */} +
+
+ {pageContent["filters-profiles-alt-icon"]} +

+ {pageContent["filters-profiles"]} +

+
+
+ + + +
+
+
+
+ + {/* Footer */} +
+ + +
+
+
+ ); +} diff --git a/src/app/(auth)/feed/components/LanguageSelector.tsx b/src/app/(auth)/feed/components/LanguageSelector.tsx new file mode 100644 index 0000000..f13117d --- /dev/null +++ b/src/app/(auth)/feed/components/LanguageSelector.tsx @@ -0,0 +1,36 @@ +import { SelectList } from './Selector'; +import LanguageIcon from '@/public/static/images/language.svg'; +import LanguageIconWhite from '@/public/static/images/language_white.svg'; +import { useApp } from '@/contexts/AppContext'; +interface LanguageSelectorProps { + languages: Record; + selectedLanguages: string[]; + onSelectLanguage: (languageId: string) => void; + placeholder?: string; +} + +export function LanguageSelector({ + languages, + selectedLanguages, + onSelectLanguage, + placeholder +}: LanguageSelectorProps) { + const { pageContent } = useApp(); + const languagesList = Object.entries(languages).map(([id, name]) => ({ + id, + name + })); + + return ( + + ); +} diff --git a/src/app/(auth)/feed/components/Selector.tsx b/src/app/(auth)/feed/components/Selector.tsx new file mode 100644 index 0000000..769bc8a --- /dev/null +++ b/src/app/(auth)/feed/components/Selector.tsx @@ -0,0 +1,110 @@ +import { useTheme } from '@/contexts/ThemeContext'; +import Image from 'next/image'; +import ArrowDownIcon from "@/public/static/images/arrow_drop_down_circle.svg"; +import ArrowDownIconWhite from "@/public/static/images/arrow_drop_down_circle_white.svg"; +import CloseIcon from "@/public/static/images/close_mobile_menu_icon_light_mode.svg"; +import CloseIconWhite from "@/public/static/images/close_mobile_menu_icon_dark_mode.svg"; +import { useApp } from '@/contexts/AppContext'; + +interface SelectListProps { + icon: string; + iconDark: string; + title: string; + items: Array<{ id: string | number; name: string }>; + selectedItems: string[] | number[]; + onSelect: (item: string | number) => void; + placeholder?: string; + multiple?: boolean; +} + +export function SelectList({ + icon, + iconDark, + title, + items, + selectedItems, + onSelect, + placeholder, + multiple = false, +}: SelectListProps) { + const { darkMode } = useTheme(); + const { pageContent } = useApp(); + + return ( +
+ {/* Header */} +
+ {`${title} +

+ {title} +

+
+ + {/* Selected Items */} + {selectedItems.length > 0 && ( +
+ {selectedItems.map((itemId) => { + const item = items.find(i => i.id === itemId); + return ( +
+ + {item?.name} + + {multiple && ( + + )} +
+ ); + })} +
+ )} + + {/* Select/Search Input */} +
+ +
+ {pageContent["filters-select-alt-icon"]} +
+
+
+ ); +} diff --git a/src/app/(auth)/feed/components/TerritorySelector.tsx b/src/app/(auth)/feed/components/TerritorySelector.tsx new file mode 100644 index 0000000..b29ff90 --- /dev/null +++ b/src/app/(auth)/feed/components/TerritorySelector.tsx @@ -0,0 +1,36 @@ +import { SelectList } from './Selector'; +import TerritoryIcon from '@/public/static/images/territory.svg'; +import TerritoryIconWhite from '@/public/static/images/territory_white.svg'; +import { useApp } from '@/contexts/AppContext'; +interface TerritorySelectorProps { + territories: Record; + selectedTerritories: string[]; + onSelectTerritory: (TerritoryId: string) => void; + placeholder?: string; +} + +export function TerritorySelector({ + territories, + selectedTerritories, + onSelectTerritory, + placeholder +}: TerritorySelectorProps) { + const { pageContent } = useApp(); + const territoriesList = Object.entries(territories).map(([id, name]) => ({ + id, + name + })); + + return ( + + ); +} diff --git a/src/app/(auth)/feed/page.tsx b/src/app/(auth)/feed/page.tsx index d0759e6..8e08a24 100644 --- a/src/app/(auth)/feed/page.tsx +++ b/src/app/(auth)/feed/page.tsx @@ -1,39 +1,202 @@ "use client"; +import { useState } from "react"; +import { useTheme } from "@/contexts/ThemeContext"; +import Image from "next/image"; import ProfileCard from "./components/Card"; +import FilterIcon from "@/public/static/images/filter_icon.svg"; +import FilterIconWhite from "@/public/static/images/filter_icon_white.svg"; +import SearchIcon from "@/public/static/images/search_icon.svg"; +import SearchIconWhite from "@/public/static/images/search_icon_white.svg"; +import CloseIcon from "@/public/static/images/close_mobile_menu_icon_light_mode.svg"; +import CloseIconWhite from "@/public/static/images/close_mobile_menu_icon_dark_mode.svg"; +import { Filters } from "./components/Filters"; +import { useApp } from "@/contexts/AppContext"; -export enum ProfileType { +export enum ProfileCapacityType { Learner = 'learner', Sharer = 'sharer', } +export enum ProfileFilterType { + Both = 'both', + User = 'user', + Organization = 'organization' +} + +export interface FilterState { + capacities: string[]; + profileCapacityTypes: ProfileCapacityType[]; + territories: string[]; + languages: string[]; + profileFilter: ProfileFilterType; +} + + export default function FeedPage() { + const [showFilters, setShowFilters] = useState(false); + const { darkMode } = useTheme(); + const { pageContent } = useApp(); + const [activeFilters, setActiveFilters] = useState({ + capacities: [] as string[], + profileCapacityTypes: [] as ProfileCapacityType[], + territories: [] as string[], + languages: [] as string[], + profileFilter: ProfileFilterType.Both + }); + const [searchCapacity, setSearchCapacity] = useState(''); + + const handleAddCapacity = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchCapacity.trim()) { + const trimmedCapacity = searchCapacity.trim(); + if (!activeFilters.capacities.includes(trimmedCapacity)) { + setActiveFilters(prev => ({ + ...prev, + capacities: [...prev.capacities, trimmedCapacity] + })); + } + setSearchCapacity(''); + } + }; + + const handleRemoveCapacity = (capacity: string) => { + setActiveFilters(prev => ({ + ...prev, + capacities: prev.capacities.filter(cap => cap !== capacity) + })); + }; + + const handleApplyFilters = (newFilters: FilterState) => { + setActiveFilters(newFilters); + setShowFilters(false); + }; + // TODO: Its a temp mock here. Get actual data from API const profiles = [ - { username: "Learner 1", capacities: [], type: ProfileType.Learner, territory: "Brazil" }, - { username: "Sharer 1", type: ProfileType.Sharer, capacities: [61] }, - { username: "Org Sharer 2", capacities: [], type: ProfileType.Sharer, languages: ["português"], avatar: "https://upload.wikimedia.org/wikipedia/commons/4/45/Wiki_Movimento_Brasil_logo.svg" }, - { username: "Sharer 3", capacities: [41, 43, 59, 61, 135], type: ProfileType.Sharer, territory: "Brazil", languages: ["português", "inglês", "espanhol", "japonês", "francês"], avatar: "https://commons.wikimedia.org/wiki/Special:Redirect/file/CapX_-_Avatar_-_3.svg" }, - { username: "Sharer With A Huge Name To Test On Device 4", capacities: [41, 43, 59, 61, 135], type: ProfileType.Sharer, territory: "Brazil", languages: ["português", "inglês", "espanhol", "japonês", "francês"], avatar: "https://commons.wikimedia.org/wiki/Special:Redirect/file/CapX_-_Avatar_-_3.svg" } + { username: "Learner 1", capacities: [], type: ProfileCapacityType.Learner, territory: "Brazil" }, + { username: "Sharer 1", type: ProfileCapacityType.Sharer, capacities: [61] }, + { username: "Org Sharer 2", capacities: [], type: ProfileCapacityType.Sharer, languages: ["português"], avatar: "https://upload.wikimedia.org/wikipedia/commons/4/45/Wiki_Movimento_Brasil_logo.svg" }, + { username: "Sharer 3", capacities: [41, 43, 59, 61, 135], type: ProfileCapacityType.Sharer, territory: "Brazil", languages: ["português", "inglês", "espanhol", "japonês", "francês"], avatar: "https://commons.wikimedia.org/wiki/Special:Redirect/file/CapX_-_Avatar_-_3.svg" }, + { username: "Sharer With A Huge Name To Test On Device 4", capacities: [41, 43, 59, 61, 135], type: ProfileCapacityType.Sharer, territory: "Brazil", languages: ["português", "inglês", "espanhol", "japonês", "francês"], avatar: "https://commons.wikimedia.org/wiki/Special:Redirect/file/CapX_-_Avatar_-_3.svg" } ]; return (
- {profiles.map((profile, index) => ( - - ))} + {/* SearchBar and Filters Button */} +
+ {/* Search Field Container */} +
+
+ {/* Search Icon */} +
+ Search +
+ + {/* Container for the selected capacities and the input */} +
+ {/* Selected Capacities */} + {activeFilters.capacities.map((capacity, index) => ( + + {capacity} + + + ))} + + {/* Search Input */} +
+ setSearchCapacity(e.target.value)} + onKeyDown={handleAddCapacity} + placeholder={activeFilters.capacities.length === 0 ? pageContent["filters-search-by-capacities"] : ''} + className={` + w-full outline-none overflow-ellipsis bg-transparent + ${darkMode ? 'text-white placeholder:text-gray-400' : 'text-gray-900 placeholder:text-gray-500'} + `} + /> +
+
+
+
+ + {/* Filters Button */} + +
+ +
+ {profiles.map((profile, index) => ( + + ))} +
+ + {/* Filters Modal */} + {showFilters && ( + setShowFilters(false)} + onApplyFilters={handleApplyFilters} + initialFilters={activeFilters} + /> + )}
); }