From a698535eb0dbe924ad8e7e2e4bbe1005df9357fc Mon Sep 17 00:00:00 2001 From: Aamil13 Date: Sun, 22 Dec 2024 14:21:05 +0530 Subject: [PATCH 1/2] university filter screen and university info screen --- next.config.js | 15 +- src/app/discover/page.tsx | 3 +- src/app/university/[id]/page.tsx | 142 ++++++++++++++++-- .../atoms/SelectDropdown/SelectDropdown.tsx | 113 +++++++++++++- .../Discover/DiscoverFilterComponent.tsx | 117 +++++++++++++++ .../Discover/DiscoverUniversityCard.tsx | 50 ++++++ .../DiscoverUniversityCardSkeleton.tsx | 16 ++ .../organisms/DiscoverContainer/index.tsx | 58 +++++++ src/services/universitySearch.tsx | 28 +++- src/utils/countriesList.ts | 2 +- 10 files changed, 521 insertions(+), 23 deletions(-) create mode 100644 src/components/molecules/Discover/DiscoverFilterComponent.tsx create mode 100644 src/components/molecules/Discover/DiscoverUniversityCard.tsx create mode 100644 src/components/molecules/Discover/DiscoverUniversityCardSkeleton.tsx create mode 100644 src/components/organisms/DiscoverContainer/index.tsx diff --git a/next.config.js b/next.config.js index 1b19f329..0bdfd8fb 100644 --- a/next.config.js +++ b/next.config.js @@ -14,8 +14,21 @@ const withPWA = require("next-pwa")({ // Next.js configuration const nextConfig = { // Allow images from external domains + // images: { + // domains: ['cdn.pixabay.com', "res.cloudinary.com", "upload.wikimedia.org", 'www.servizisegreti.com',"saig.physics.ualberta.ca"], + // }, + images: { - domains: ['cdn.pixabay.com', "res.cloudinary.com", "upload.wikimedia.org", 'www.servizisegreti.com',"saig.physics.ualberta.ca"], + remotePatterns: [ + { + protocol: 'https', + hostname: '**', // Accepts all domains + }, + { + protocol: 'http', + hostname: '**', // Accepts all domains + }, + ], }, // Custom headers for API routes diff --git a/src/app/discover/page.tsx b/src/app/discover/page.tsx index bd71455a..550d5aea 100644 --- a/src/app/discover/page.tsx +++ b/src/app/discover/page.tsx @@ -1,7 +1,8 @@ +import DiscoverContainer from '@/components/organisms/DiscoverContainer' import React from 'react' function Discover() { - return
Discover
+ return } export default Discover diff --git a/src/app/university/[id]/page.tsx b/src/app/university/[id]/page.tsx index fa2a57c8..53ada4eb 100644 --- a/src/app/university/[id]/page.tsx +++ b/src/app/university/[id]/page.tsx @@ -1,38 +1,154 @@ 'use client' +import Buttons from '@/components/atoms/Buttons' import Loading from '@/components/atoms/Loading' -import { useUniversitySearch, useUniversitySearchByName } from '@/services/universitySearch' -import { UniversityInfo } from '@/types/University' -import ImagePlaceholder from '@assets/unibuzz-orange.png' +import { useUniversitySearchByName } from '@/services/universitySearch' +import Image from 'next/image' import { useParams } from 'next/navigation' +import { MdEmail } from 'react-icons/md' +import React, { useState } from 'react' +import { FaPhoneAlt } from 'react-icons/fa' +import { MdFax } from 'react-icons/md' +import { IoIosLink } from 'react-icons/io' +import { PiBuildingsFill } from 'react-icons/pi' +import { BsClockFill } from 'react-icons/bs' +import universityPlaceholder from '@assets/university_placeholder.jpg' +import universityLogoPlaceholder from '@assets/unibuzz_rounded.svg' +import { IconType } from 'react-icons/lib' -import React, { useEffect } from 'react' +const UniversityCard = ({ icon: Icon, title, info }: { icon: IconType; title: string; info: string }) => ( +
+

+ + {title} +

+

{info || 'Not available'}

+
+) export default function UniversityProfile() { const params = useParams() const { id: universityName } = params const { data: university, isLoading: isUniversityLoading } = useUniversitySearchByName(universityName as string) + + const [imageSrc, setImageSrc] = useState(university?.images[0] || universityPlaceholder) + const [logoSrc, setLogoSrc] = useState(university?.logos?.[0] || universityLogoPlaceholder) + if (isUniversityLoading) return + + const contactData = [ + { + icon: MdEmail, + title: 'Email', + info: university.wikiInfoBox?.email, + }, + { + icon: FaPhoneAlt, + title: 'Phone', + info: university.collegeBoardInfo?.['Phone number'], + }, + { + icon: MdFax, + title: 'Fax', + info: university.wikiInfoBox?.fax, + }, + ] + + const additionalData = [ + { + icon: IoIosLink, + title: 'Link', + info: university.collegeBoardInfo?.website, + }, + { + icon: PiBuildingsFill, + title: 'Address', + info: university.collegeBoardInfo?.Location, + }, + { + icon: BsClockFill, + title: 'Office Hours', + info: university.wikiInfoBox?.['Office Hours'] || 'Monday to Friday 9:00 am - 12:00 p.m. and 1:00 p.m - 5:00 p.m', + }, + ] + return ( -
-
-
+
+
+
- logo setLogoSrc(universityLogoPlaceholder)} />
-

{university?.name}

+

{university?.name}

-

{university?.topUniInfo?.about}

+

{university?.topUniInfo?.about || 'Not Available'}

+ {university?.isCommunityCreated ? Join Community : Endorse} +
+
+ university_image setImageSrc(universityPlaceholder)} + />
-
- university_image +
+ {/* //overview */} +
+

Overview

+
+

+ Loremium University boasts a world-class faculty, many of whom are leaders in their respective fields. The university’s commitment to + innovation and research has led to groundbreaking discoveries and advancements, particularly in the realms of biotechnology, environmental + science, and digital arts. The state-of-the-art laboratories and creative studios provide students with the resources and inspiration + needed to push the boundaries of knowledge and creativity. +

+

+ In addition to its scientific prowess, Loremium University is a thriving cultural hub. The university’s School of Arts is renowned for its + avant-garde approach to art and design, producing graduates who have gone on to achieve international acclaim. Regular exhibitions, + performances, and lectures by visiting artists and scholars enrich the campus life and foster a dynamic exchange of ideas. +

+

+ Loremium University is a magnet for students from around the globe, drawn by its stellar reputation and welcoming atmosphere. The + university’s diverse student body, hailing from over 80 countries, creates a rich tapestry of cultures and perspectives. Dedicated support + services ensure that international students feel at home, making their transition to life in Loremium seamless and enjoyable. +

+
+
+ {/* //contact */} +
+

Contact Info

+
+
+ {contactData.map((item, index) => ( + + ))} +
+
+ {additionalData.map((item, index) => ( + + ))} +
+
+
+ {/* //contact */} +
+

Reviews

+
+

Reviews are coming soon!

+

+ This feature is under construction. If you would like to have it sooner send us some feedback! +

diff --git a/src/components/atoms/SelectDropdown/SelectDropdown.tsx b/src/components/atoms/SelectDropdown/SelectDropdown.tsx index 618fd98a..0408473b 100644 --- a/src/components/atoms/SelectDropdown/SelectDropdown.tsx +++ b/src/components/atoms/SelectDropdown/SelectDropdown.tsx @@ -25,6 +25,100 @@ const motionStyle = { exit: { opacity: 0, y: '-10%', transition: { duration: '0.35' } }, transition: { type: 'spring', stiffness: '100', duration: '0.75' }, } +// const SelectDropdown = ({ options, onChange, value, placeholder, icon, search = false, err, showIcon = false }: SelectDropdownProps) => { +// const [show, setShow] = useState(false) +// const dropdownRef = useRef(null) +// const [filteredOptions, setFilteredOptions] = useState(options) +// const searchRef = useRef(null) + +// const handleSelect = (optionValue: string) => { +// onChange(optionValue) +// setShow(false) +// } + +// const handleSearch = () => { +// const searchValue = searchRef.current?.value.toLowerCase() || '' +// setFilteredOptions(searchValue === '' ? options : options.filter((option: string) => option.toLowerCase().includes(searchValue))) +// } + +// useEffect(() => { +// const handleClickOutside = (event: MouseEvent) => { +// if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { +// setShow(false) +// } +// } + +// document.addEventListener('mousedown', handleClickOutside) + +// return () => { +// document.removeEventListener('mousedown', handleClickOutside) +// } +// }, []) + +// return ( +// +//
setShow(!show)} +// className={`${ +// err ? 'border-red-400' : 'border-neutral-200' +// } flex justify-between items-center py-2 px-3 border focus:ring-2 rounded-lg drop-shadow-sm text-neutral-400 outline-none`} +// > +//

{value || placeholder}

+//
+// {icon == 'single' ? ( +// +// ) : ( +//
+// +// +//
+// )} +//
+//
+// +// {show && ( +// +// {search && ( +// +// )} +// {filteredOptions?.length > 0 ? ( +// filteredOptions?.map((item: string, key: number) => { +// const IconComponent = icons[key % icons.length] +// return ( +//
handleSelect(item)} +// key={key} +// > +// {showIcon && } +//

{item}

+//
+// ) +// }) +// ) : ( +//

No results found

+// )} +//
+// )} +//
+//
+// ) +// } + +// export default SelectDropdown const SelectDropdown = ({ options, onChange, value, placeholder, icon, search = false, err, showIcon = false }: SelectDropdownProps) => { const [show, setShow] = useState(false) const dropdownRef = useRef(null) @@ -55,17 +149,25 @@ const SelectDropdown = ({ options, onChange, value, placeholder, icon, search = } }, []) + const toggleDropdown = () => { + if (!show) { + setFilteredOptions(options) + if (searchRef.current) searchRef.current.value = '' + } + setShow((prevShow) => !prevShow) + } + return (
setShow(!show)} + onClick={toggleDropdown} className={`${ err ? 'border-red-400' : 'border-neutral-200' } flex justify-between items-center py-2 px-3 border focus:ring-2 rounded-lg drop-shadow-sm text-neutral-400 outline-none`} >

{value || placeholder}

- {icon == 'single' ? ( + {icon === 'single' ? ( ) : (
@@ -78,9 +180,9 @@ const SelectDropdown = ({ options, onChange, value, placeholder, icon, search = {show && ( {search && ( @@ -93,7 +195,7 @@ const SelectDropdown = ({ options, onChange, value, placeholder, icon, search = /> )} {filteredOptions?.length > 0 ? ( - filteredOptions?.map((item: string, key: number) => { + filteredOptions.map((item: string, key: number) => { const IconComponent = icons[key % icons.length] return (
) } - export default SelectDropdown diff --git a/src/components/molecules/Discover/DiscoverFilterComponent.tsx b/src/components/molecules/Discover/DiscoverFilterComponent.tsx new file mode 100644 index 00000000..664f3434 --- /dev/null +++ b/src/components/molecules/Discover/DiscoverFilterComponent.tsx @@ -0,0 +1,117 @@ +'use client' +import Buttons from '@/components/atoms/Buttons' +import SelectDropdown from '@/components/atoms/SelectDropdown/SelectDropdown' +import { useGetFilteredUniversity } from '@/services/universitySearch' +import { country_list } from '@/utils/countriesList' +import React, { useState } from 'react' +import { Controller, useForm } from 'react-hook-form' +import { IoIosSearch } from 'react-icons/io' + +type Props = { + setQuery: (value: any) => void +} + +const DiscoverFilterComponent = ({ setQuery }: Props) => { + const { register, control, handleSubmit: handleFormSubmit, watch, reset } = useForm() + const [cityOptions, setCityOptions] = useState([]) + const [isCityAvailable, setIsCityAvailable] = useState(true) + const currCountry = watch('country') || '' + + const handleCountryChange = (selectedCountry: string, field: any) => { + field.onChange(selectedCountry) + const cities = country_list[selectedCountry] || [] + + setCityOptions(cities.length > 0 ? cities : ['Not available']) + setIsCityAvailable(cities.length > 0) + } + + const handleFilterSubmit = (data: any) => { + setQuery(JSON.stringify(data)) + } + return ( +
+
+

Search Filter

+
+
+ + +
+
+ ( + + )} + /> +
+
+ ( + handleCountryChange(selectedCountry, field)} + placeholder="Country" + icon={'single'} + search={true} + err={false} + /> + )} + /> +
+
+ ( + + )} + /> +
+
+ ( + + )} + /> +
+
+
+ Search + reset()} size="extra_small"> + Reset + +
+
+
+ ) +} + +export default DiscoverFilterComponent diff --git a/src/components/molecules/Discover/DiscoverUniversityCard.tsx b/src/components/molecules/Discover/DiscoverUniversityCard.tsx new file mode 100644 index 00000000..d817e0d4 --- /dev/null +++ b/src/components/molecules/Discover/DiscoverUniversityCard.tsx @@ -0,0 +1,50 @@ +'use client' +import Image from 'next/image' +import React, { useState } from 'react' +import universityPlaceholder from '@assets/university_placeholder.jpg' +import universityLogoPlaceholder from '@assets/unibuzz_rounded.svg' +import { useRouter } from 'next/navigation' + +type Props = { + data: { + images: string[] + logos: string[] + name: string + pathUrl: string + } +} +const DiscoverUniversityCard = ({ data }: Props) => { + const [imageSrc, setImageSrc] = useState(data?.images[0] || universityPlaceholder) + const [logoSrc, setLogoSrc] = useState(data?.logos[0] || universityLogoPlaceholder) + + const router = useRouter() + return ( +
router.push(`/university/${data.pathUrl}`)} + className="w-96 max-xl:w-80 max-lg:w-60 max-md:w-80 relative rounded-2xl cursor-pointer" + > + {'university'} setImageSrc(universityPlaceholder)} + /> +
+ {'logo'} setLogoSrc(universityLogoPlaceholder)} + /> +

{data?.name}

+
+
+ ) +} + +export default DiscoverUniversityCard diff --git a/src/components/molecules/Discover/DiscoverUniversityCardSkeleton.tsx b/src/components/molecules/Discover/DiscoverUniversityCardSkeleton.tsx new file mode 100644 index 00000000..cdd6bc06 --- /dev/null +++ b/src/components/molecules/Discover/DiscoverUniversityCardSkeleton.tsx @@ -0,0 +1,16 @@ +import React from 'react' + +const DiscoverUniversityCardSkeleton = () => { + return ( +
+
+ +
+
+

+
+
+ ) +} + +export default DiscoverUniversityCardSkeleton diff --git a/src/components/organisms/DiscoverContainer/index.tsx b/src/components/organisms/DiscoverContainer/index.tsx new file mode 100644 index 00000000..53344726 --- /dev/null +++ b/src/components/organisms/DiscoverContainer/index.tsx @@ -0,0 +1,58 @@ +'use client' +import DiscoverFilterComponent from '@/components/molecules/Discover/DiscoverFilterComponent' +import DiscoverUniversityCard from '@/components/molecules/Discover/DiscoverUniversityCard' +import DiscoverUniversityCardSkeleton from '@/components/molecules/Discover/DiscoverUniversityCardSkeleton' +import { useGetFilteredUniversity } from '@/services/universitySearch' +import React, { useEffect, useRef, useState } from 'react' + +const DiscoverContainer = () => { + const [query, setQuery] = useState('') + const { data, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading } = useGetFilteredUniversity(5, query) + + const universities = data?.pages.flatMap((page) => page.Universities) || [] + + const containerRef = useRef(null) + + useEffect(() => { + const handleScroll = () => { + if (containerRef.current) { + const { scrollTop, scrollHeight, clientHeight } = containerRef.current + + if (scrollTop + clientHeight >= scrollHeight - 10 && hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + } + } + + const container = containerRef.current + container?.addEventListener('scroll', handleScroll) + + return () => { + container?.removeEventListener('scroll', handleScroll) + } + }, [fetchNextPage, hasNextPage, isFetchingNextPage]) + + return ( +
+ + +
+ {isLoading ? ( + <> + + + + + + + ) : !isLoading && !universities.length ? ( +
No Result
+ ) : ( + universities?.map((item) => ) + )} +
+
+ ) +} + +export default DiscoverContainer diff --git a/src/services/universitySearch.tsx b/src/services/universitySearch.tsx index 4a6279ca..6928d262 100644 --- a/src/services/universitySearch.tsx +++ b/src/services/universitySearch.tsx @@ -1,5 +1,5 @@ import useDebounce from '@/hooks/useDebounce' -import { useQuery } from '@tanstack/react-query' +import { useInfiniteQuery, useQuery } from '@tanstack/react-query' import { client } from './api-Client' import { ServerResponse } from '@/models/common/api-client' @@ -44,3 +44,29 @@ export async function getUniversityByName(universityName: string): Promise getFilteredUniversity(pageParam, limit, query), + getNextPageParam: (lastPage) => { + if (lastPage.currentPage < lastPage.totalPages) { + return lastPage.currentPage + 1 + } + return undefined + }, + initialPageParam: 1, + }) +} diff --git a/src/utils/countriesList.ts b/src/utils/countriesList.ts index 06603aee..08b98cd5 100644 --- a/src/utils/countriesList.ts +++ b/src/utils/countriesList.ts @@ -10,7 +10,7 @@ export const country_list: Record = { Armenia: [], Aruba: [], Australia: [], - Austria: [], + Austria: ['Styria', 'Vienna'], Azerbaijan: [], Bahamas: [], Bahrain: [], From 04a663771d13dabc42e12aeac5124d62f0431b9a Mon Sep 17 00:00:00 2001 From: Aamil13 Date: Sun, 22 Dec 2024 14:28:46 +0530 Subject: [PATCH 2/2] removed comments --- next.config.js | 4 - .../atoms/SelectDropdown/SelectDropdown.tsx | 93 ------------------- 2 files changed, 97 deletions(-) diff --git a/next.config.js b/next.config.js index 0bdfd8fb..e9ec4232 100644 --- a/next.config.js +++ b/next.config.js @@ -13,10 +13,6 @@ const withPWA = require("next-pwa")({ // Next.js configuration const nextConfig = { - // Allow images from external domains - // images: { - // domains: ['cdn.pixabay.com', "res.cloudinary.com", "upload.wikimedia.org", 'www.servizisegreti.com',"saig.physics.ualberta.ca"], - // }, images: { remotePatterns: [ diff --git a/src/components/atoms/SelectDropdown/SelectDropdown.tsx b/src/components/atoms/SelectDropdown/SelectDropdown.tsx index 0408473b..4def7983 100644 --- a/src/components/atoms/SelectDropdown/SelectDropdown.tsx +++ b/src/components/atoms/SelectDropdown/SelectDropdown.tsx @@ -25,100 +25,7 @@ const motionStyle = { exit: { opacity: 0, y: '-10%', transition: { duration: '0.35' } }, transition: { type: 'spring', stiffness: '100', duration: '0.75' }, } -// const SelectDropdown = ({ options, onChange, value, placeholder, icon, search = false, err, showIcon = false }: SelectDropdownProps) => { -// const [show, setShow] = useState(false) -// const dropdownRef = useRef(null) -// const [filteredOptions, setFilteredOptions] = useState(options) -// const searchRef = useRef(null) -// const handleSelect = (optionValue: string) => { -// onChange(optionValue) -// setShow(false) -// } - -// const handleSearch = () => { -// const searchValue = searchRef.current?.value.toLowerCase() || '' -// setFilteredOptions(searchValue === '' ? options : options.filter((option: string) => option.toLowerCase().includes(searchValue))) -// } - -// useEffect(() => { -// const handleClickOutside = (event: MouseEvent) => { -// if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { -// setShow(false) -// } -// } - -// document.addEventListener('mousedown', handleClickOutside) - -// return () => { -// document.removeEventListener('mousedown', handleClickOutside) -// } -// }, []) - -// return ( -// -//
setShow(!show)} -// className={`${ -// err ? 'border-red-400' : 'border-neutral-200' -// } flex justify-between items-center py-2 px-3 border focus:ring-2 rounded-lg drop-shadow-sm text-neutral-400 outline-none`} -// > -//

{value || placeholder}

-//
-// {icon == 'single' ? ( -// -// ) : ( -//
-// -// -//
-// )} -//
-//
-// -// {show && ( -// -// {search && ( -// -// )} -// {filteredOptions?.length > 0 ? ( -// filteredOptions?.map((item: string, key: number) => { -// const IconComponent = icons[key % icons.length] -// return ( -//
handleSelect(item)} -// key={key} -// > -// {showIcon && } -//

{item}

-//
-// ) -// }) -// ) : ( -//

No results found

-// )} -//
-// )} -//
-//
-// ) -// } - -// export default SelectDropdown const SelectDropdown = ({ options, onChange, value, placeholder, icon, search = false, err, showIcon = false }: SelectDropdownProps) => { const [show, setShow] = useState(false) const dropdownRef = useRef(null)