From 55587963a6de8129079178eafc30392824d58fdf Mon Sep 17 00:00:00 2001 From: Anurag Negi <115611556+anuragnegi000@users.noreply.github.com> Date: Thu, 29 Aug 2024 19:58:41 +0530 Subject: [PATCH] fix: Improvement: Landing page improvement #271 (#278) * removed login and ModeToggle button(for dark/light mode switch) added infinite scroll for companies logo added debouncing and skeleton in place of jobs on load of screen * removed unwanted images * fixes --- package.json | 1 + public/google.svg | 10 ++ public/microsoft.svg | 10 ++ public/solana.svg | 12 ++ src/app/globals.css | 209 +++++++++++++++++++++++++++++ src/components/BackgroundSvg.tsx | 2 +- src/components/hero-section.tsx | 11 +- src/components/infinitescroll.tsx | 115 ++++++++++++++++ src/components/job-card-loader.tsx | 37 +++++ src/components/job-landing.tsx | 10 +- src/components/ui/Marquee.tsx | 52 +++++++ src/components/ui/skeleton.tsx | 15 +++ src/layouts/header.tsx | 31 ++--- src/layouts/jobs-header.tsx | 42 +++++- tailwind.config.ts | 21 ++- 15 files changed, 531 insertions(+), 47 deletions(-) create mode 100644 public/google.svg create mode 100644 public/microsoft.svg create mode 100644 public/solana.svg create mode 100644 src/components/infinitescroll.tsx create mode 100644 src/components/job-card-loader.tsx create mode 100644 src/components/ui/Marquee.tsx create mode 100644 src/components/ui/skeleton.tsx diff --git a/package.json b/package.json index da59a0e3..940ac277 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", + "@uidotdev/usehooks": "^2.4.1", "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", diff --git a/public/google.svg b/public/google.svg new file mode 100644 index 00000000..95932e27 --- /dev/null +++ b/public/google.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/public/microsoft.svg b/public/microsoft.svg new file mode 100644 index 00000000..89fba50e --- /dev/null +++ b/public/microsoft.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/public/solana.svg b/public/solana.svg new file mode 100644 index 00000000..5a68bf0f --- /dev/null +++ b/public/solana.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/app/globals.css b/src/app/globals.css index e4aecf04..6228a08a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,6 +3,9 @@ @tailwind utilities; @layer base { + .paused { + animation-play-state: paused; + } :root { --stroke-primary: 0 0% 90%; --stroke-secondary: 0 0% 92.44%; @@ -88,3 +91,209 @@ stroke-dashoffset: 0; } } +@keyframes scroll { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-100%); + } +} + +.wrapper { + margin-top: 2rem; + width: 90%; + max-width: 1536px; + margin-inline: auto; + height: 200px; + position: relative; + overflow: hidden; + mask-image: linear-gradient( + to right, + rgba(0, 0, 0, 0), + rgba(0, 0, 0, 1) 5%, + rgba(0, 0, 0, 1) 95%, + rgba(0, 0, 0, 0) + ); +} +.itemLeft img { + height: 100%; + width: 100%; + border-radius: 10px; +} +.itemRight img { + height: 100%; + width: 100%; + border-radius: 10px; +} + +@keyframes scrollLeft { + to { + left: -200px; + } +} + +@keyframes scrollRight { + to { + right: -200px; + } +} + +.itemLeft, +.itemRight { + width: 200px; + height: 180px; + /* background-color: #e11d48; */ + border-radius: 10px; + position: absolute; + animation-timing-function: linear; + animation-duration: 30s; + animation-iteration-count: infinite; +} + +.itemLeft { + left: max(calc(200px * 8), 100%); + animation-name: scrollLeft; +} + +.itemRight { + right: max(calc(200px * 8), calc(100% + 200px)); + animation-name: scrollRight; +} + +.item1 { + animation-delay: calc(30s / 8 * (8 - 1) * -1); +} + +.item2 { + animation-delay: calc(30s / 8 * (8 - 2) * -1); +} + +.item3 { + animation-delay: calc(30s / 8 * (8 - 3) * -1); +} + +.item4 { + animation-delay: calc(30s / 8 * (8 - 4) * -1); +} + +.item5 { + animation-delay: calc(30s / 8 * (8 - 5) * -1); +} + +.item6 { + animation-delay: calc(30s / 8 * (8 - 6) * -1); +} + +.item7 { + animation-delay: calc(30s / 8 * (8 - 7) * -1); +} + +.item8 { + animation-delay: calc(30s / 8 * (8 - 8) * -1); +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .wrapper { + height: 150px; + } + + .itemLeft, + .itemRight { + width: 150px; + height: 130px; + } + + .itemLeft { + left: max(calc(150px * 8), 100%); + } + + .itemRight { + right: max(calc(150px * 8), calc(100% + 150px)); + } +} + +@media (max-width: 480px) { + .wrapper { + height: 100px; + } + + .itemLeft, + .itemRight { + width: 100px; + height: 80px; + } + + .itemLeft { + left: max(calc(100px * 8), 100%); + } + + .itemRight { + right: max(calc(100px * 8), calc(100% + 100px)); + } +} +.scroll-container { + overflow: hidden; + white-space: nowrap; + width: 100%; + position: relative; +} + +.scroll-content { + display: flex; + animation: scroll 30s linear infinite; +} + +.scroll-content::after { + content: ''; + display: flex; + animation: scroll 30s linear infinite; +} + +.scroll-content a { + flex: 0 0 auto; +} + +@keyframes scroll { + 0% { + transform: translateX(0%); + } + 100% { + transform: translateX(-50%); + } +} + +/* For small devices */ +@media (max-width: 600px) { + .scroll-content, + .scroll-content::after { + animation: scroll-small 1s linear infinite; + } + + @keyframes scroll-small { + 0% { + transform: translateX(0%); + } + 100% { + transform: translateX(-50%); + } + } +} +.loader { + border: 8px solid #f3f3f3; + border-radius: 50%; + border-top: 8px solid #3498db; + width: 40px; + height: 40px; + animation: spin 2s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/src/components/BackgroundSvg.tsx b/src/components/BackgroundSvg.tsx index c492b6c3..dcf5c381 100644 --- a/src/components/BackgroundSvg.tsx +++ b/src/components/BackgroundSvg.tsx @@ -12,7 +12,7 @@ export default function BackgroundSvg() { left: 0, zIndex: -1, minHeight: '100vh', - minWidth: '100vw', + maxWidth: '100vw', objectFit: 'cover', overflow: 'hidden', }} diff --git a/src/components/hero-section.tsx b/src/components/hero-section.tsx index 5f216a17..097f83c1 100644 --- a/src/components/hero-section.tsx +++ b/src/components/hero-section.tsx @@ -1,7 +1,7 @@ import { GITHUB_REPO } from '@/lib/constant/app.constant'; import Link from 'next/link'; import Icon from './ui/icon'; -import Image from 'next/image'; +import { MarqueeDemo } from './infinitescroll'; const HeroSection = () => { return ( @@ -35,14 +35,7 @@ const HeroSection = () => { -
- companies -
+ ); diff --git a/src/components/infinitescroll.tsx b/src/components/infinitescroll.tsx new file mode 100644 index 00000000..78155f28 --- /dev/null +++ b/src/components/infinitescroll.tsx @@ -0,0 +1,115 @@ +import React from 'react'; +import Marquee from './ui/Marquee'; +import Image from 'next/image'; +// import Marquee from "@/components/magicui/marquee"; + +const reviews = [ + { + name: 'Jack', + username: '@jack', + body: "I've never seen anything like this before. It's amazing. I love it.", + img: 'https://avatar.vercel.sh/jack', + }, + { + name: 'Jill', + username: '@jill', + body: "I don't know what to say. I'm speechless. This is amazing.", + img: 'https://avatar.vercel.sh/jill', + }, + { + name: 'John', + username: '@john', + body: "I'm at a loss for words. This is amazing. I love it.", + img: 'https://avatar.vercel.sh/john', + }, + { + name: 'Jane', + username: '@jane', + body: "I'm at a loss for words. This is amazing. I love it.", + img: 'https://avatar.vercel.sh/jane', + }, + { + name: 'Jenny', + username: '@jenny', + body: "I'm at a loss for words. This is amazing. I love it.", + img: 'https://avatar.vercel.sh/jenny', + }, + { + name: 'James', + username: '@james', + body: "I'm at a loss for words. This is amazing. I love it.", + img: 'https://avatar.vercel.sh/james', + }, +]; + +const imageList = [ + { + id: 1, + src: './microsoft.svg', + }, + { + id: 2, + src: './solana.svg', + }, + { + id: 3, + src: './google.svg', + }, +]; + +const firstRow = reviews.slice(0, reviews.length / 2); + +const ReviewCard = ({}) => { + return ( +
+ {imageList.concat(imageList).map((item, index) => { + return ( + companies + ); + })} +
+ ); +}; + +export function MarqueeDemo() { + return ( +
+ + {firstRow.map((review) => ( + + ))} + + + {/*
+
*/} +
+ ); +} + +//
+// {imageList.concat(imageList).map((item, index) => ( +// companies +// ))} +//
diff --git a/src/components/job-card-loader.tsx b/src/components/job-card-loader.tsx new file mode 100644 index 00000000..2ae82b45 --- /dev/null +++ b/src/components/job-card-loader.tsx @@ -0,0 +1,37 @@ +import { Skeleton } from '@/components/ui/skeleton'; +import React from 'react'; + +const JobCardLoader = () => { + return ( +
+
+
+ +
+
+

+ +

+

+ +

+
+
+
+
+ +
+
+

+ +

+

+ +

+
+
+
+ ); +}; + +export default JobCardLoader; diff --git a/src/components/job-landing.tsx b/src/components/job-landing.tsx index 6df56fc6..e986c8a9 100644 --- a/src/components/job-landing.tsx +++ b/src/components/job-landing.tsx @@ -1,11 +1,11 @@ import { getAllJobs } from '@/actions/job.action'; import { formatSalary } from '@/lib/utils'; import Link from 'next/link'; +import JobCardLoader from '@/components/job-card-loader'; import { DEFAULT_PAGE, JOBS_PER_PAGE } from '@/config/app.config'; import JobsHeader from '@/layouts/jobs-header'; import { Suspense } from 'react'; -import { Loader } from 'lucide-react'; import { JobQuerySchemaType } from '@/lib/validators/jobs.validator'; import { Pagination, PaginationContent, PaginationItem } from './ui/pagination'; import { @@ -32,13 +32,7 @@ export const JobLanding = async ({
- - -
- } - > + }>
diff --git a/src/components/ui/Marquee.tsx b/src/components/ui/Marquee.tsx new file mode 100644 index 00000000..98a3a56f --- /dev/null +++ b/src/components/ui/Marquee.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { cn } from '../../lib/utils'; + +interface MarqueeProps { + className?: string; + reverse?: boolean; + pauseOnHover?: boolean; + children?: React.ReactNode; + vertical?: boolean; + repeat?: number; + [key: string]: any; +} + +export default function Marquee({ + className, + reverse, + pauseOnHover = false, + children, + vertical = false, + repeat = 4, + ...props +}: MarqueeProps) { + return ( +
+ {Array(repeat) + .fill(0) + .map((_, i) => ( +
+ {children} +
+ ))} +
+ ); +} diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 00000000..a626d9ba --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from '@/lib/utils'; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; diff --git a/src/layouts/header.tsx b/src/layouts/header.tsx index 23fce6d1..1686ab2c 100644 --- a/src/layouts/header.tsx +++ b/src/layouts/header.tsx @@ -1,13 +1,11 @@ 'use client'; import { MobileNav } from '@/layouts/mobile-nav'; -import { ModeToggle } from '@/components/ui/theme-toggle'; -import APP_PATHS from '@/config/path.config'; import { navbar } from '@/lib/constant/app.constant'; import { useSession } from 'next-auth/react'; import Link from 'next/link'; -import { ProfileMenu } from '@/components/profile-menu'; import { NavItem } from '@/components/navitem'; import Image from 'next/image'; +import { Skeleton } from '@/components/ui/skeleton'; const CompanyLogo = () => { return ( @@ -39,28 +37,15 @@ const Header = () => {
- -
- {session.status !== 'loading' && !session.data?.user && ( - <> - - Login - - - )} - {session.status !== 'loading' && session.data?.user && ( - - )} - -
diff --git a/src/layouts/jobs-header.tsx b/src/layouts/jobs-header.tsx index 1daf3ef2..125bd4ec 100644 --- a/src/layouts/jobs-header.tsx +++ b/src/layouts/jobs-header.tsx @@ -17,6 +17,8 @@ import { JobQuerySchemaType } from '@/lib/validators/jobs.validator'; import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { useRouter } from 'next/navigation'; + import { Form, FormControl, @@ -24,12 +26,13 @@ import { FormItem, FormMessage, } from '@/components/ui/form'; +import { useDebounce } from '@uidotdev/usehooks'; import { Input } from '@/components/ui/input'; import { usePathname } from 'next/navigation'; import JobFilters from './job-filters'; import Icon from '@/components/ui/icon'; import APP_PATHS from '@/config/path.config'; - +import { useEffect } from 'react'; const FormSchema = z.object({ search: z.string().optional(), }); @@ -43,6 +46,11 @@ const JobsHeader = ({ }) => { const pathname = usePathname(); const isHome = pathname === APP_PATHS.HOME; + const router = useRouter(); + + function sortChangeHandler(value: SortByEnums) { + jobFilterQuery({ ...searchParams, sortby: value, page: 1 }, baseUrl); + } let debounceTimeout: NodeJS.Timeout; @@ -52,11 +60,35 @@ const JobsHeader = ({ search: '', }, }); + + const searchValue = form.watch('search'); + const debouncedSearchValue = useDebounce(searchValue, 100); + + useEffect(() => { + const fetch = async () => { + if (debouncedSearchValue !== 'undefined') { + if (debouncedSearchValue?.length) { + await onSubmit({ search: debouncedSearchValue }); + } else { + router.push(baseUrl); + } + } else { + router.push(baseUrl); + } + }; + + fetch(); + }, [debouncedSearchValue]); + function onSubmit(data: z.infer) { - jobFilterQuery({ ...searchParams, search: data.search, page: 1 }, baseUrl); - } - function sortChangeHandler(value: SortByEnums) { - jobFilterQuery({ ...searchParams, sortby: value, page: 1 }, baseUrl); + jobFilterQuery( + { + ...searchParams, + search: data.search, + page: 1, + }, + baseUrl + ); } return ( diff --git a/tailwind.config.ts b/tailwind.config.ts index 201536c3..38354fd9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -26,7 +26,6 @@ const config = { backgroundImage: { 'blue-gradient': 'radial-gradient(circle, #3b82f6, #1e3a8a)', }, - colors: { 'stroke-primary': 'hsl(var(--stroke-primary))', 'stroke-secondary': 'hsl(var(--stroke-secondary))', @@ -70,6 +69,14 @@ const config = { sm: 'calc(var(--radius) - 4px)', }, keyframes: { + marquee: { + from: { transform: 'translateX(0)' }, + to: { transform: 'translateX(calc(-100% - var(--gap)))' }, + }, + 'marquee-vertical': { + from: { transform: 'translateY(0)' }, + to: { transform: 'translateY(calc(-100% - var(--gap)))' }, + }, 'accordion-down': { from: { height: '0' }, to: { height: 'var(--radix-accordion-content-height)' }, @@ -78,10 +85,22 @@ const config = { from: { height: 'var(--radix-accordion-content-height)' }, to: { height: '0' }, }, + scroll: { + '0%': { transform: 'translateX(0)' }, + '100%': { transform: 'translateX(-100%)' }, + }, + 'loop-scroll': { + from: { transform: 'translateX(0%)' }, + to: { transform: 'translateX(-100%)' }, + }, }, animation: { + marquee: 'marquee var(--duration) linear infinite', + 'marquee-vertical': 'marquee-vertical var(--duration) linear infinite', 'accordion-down': 'accordion-down 0.2s ease-out', 'accordion-up': 'accordion-up 0.2s ease-out', + scroll: 'scroll 5s linear infinite', + 'loop-scroll': 'loop-scroll 50s linear infinite', }, }, },