diff --git a/package-lock.json b/package-lock.json index 21ec76c..ece06e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6449,21 +6449,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", - "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/text-to-bullets": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/text-to-bullets/-/text-to-bullets-1.0.6.tgz", diff --git a/src/pages/ResourcesPage/images/block_gradient.png b/src/pages/ResourcesPage/images/block_gradient.png new file mode 100644 index 0000000..199d483 Binary files /dev/null and b/src/pages/ResourcesPage/images/block_gradient.png differ diff --git a/src/pages/ResourcesPage/images/folder-bg-full.png b/src/pages/ResourcesPage/images/folder-bg-full.png new file mode 100644 index 0000000..4683dc9 Binary files /dev/null and b/src/pages/ResourcesPage/images/folder-bg-full.png differ diff --git a/src/pages/ResourcesPage/images/lock card.png b/src/pages/ResourcesPage/images/lock card.png new file mode 100644 index 0000000..8efea13 Binary files /dev/null and b/src/pages/ResourcesPage/images/lock card.png differ diff --git a/src/pages/ResourcesPage/images/substr_white.png b/src/pages/ResourcesPage/images/substr_white.png new file mode 100644 index 0000000..d886509 Binary files /dev/null and b/src/pages/ResourcesPage/images/substr_white.png differ diff --git a/src/pages/ResourcesPage/images/sustr_roz.png b/src/pages/ResourcesPage/images/sustr_roz.png new file mode 100644 index 0000000..bd1111d Binary files /dev/null and b/src/pages/ResourcesPage/images/sustr_roz.png differ diff --git a/src/pages/ResourcesPage/index.ts b/src/pages/ResourcesPage/index.ts new file mode 100644 index 0000000..e1a29bf --- /dev/null +++ b/src/pages/ResourcesPage/index.ts @@ -0,0 +1,3 @@ +import { ResourcesPage } from './ui/ResourcesPage.tsx'; + +export { ResourcesPage }; diff --git a/src/pages/ResourcesPage/ui/ResourceCard.module.css b/src/pages/ResourcesPage/ui/ResourceCard.module.css new file mode 100644 index 0000000..24e6ed1 --- /dev/null +++ b/src/pages/ResourcesPage/ui/ResourceCard.module.css @@ -0,0 +1,142 @@ +.cardContainer { + background-image: + linear-gradient( + 180deg, + rgba(253, 225, 190, 0) 0%, + rgba(253, 225, 190, 0) 120px, + rgba(253, 225, 190, 1) 220px, + rgba(245, 181, 203, 1) 100% + ), + /*my original image*/ + url('../images/folder-bg-full.png'); + + background-repeat: no-repeat, no-repeat; + background-size: 100% 100%, 100% auto; + background-position: top center, top center; + + border-radius: 0 0 24px 24px; + overflow: hidden; + + padding-top: 65px; + padding-left: 25px; + padding-right: 25px; + padding-bottom: 30px; + + display: flex; + flex-direction: column; + gap: 15px; + min-height: 350px; + + /*white border*/ + position: relative; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); + + break-inside: avoid; + margin-bottom: 20px; +} + +/*cutting the border on the top*/ +.cardContainer::after { + content: ""; + position: absolute; + top: 60px; + + left: 0; + right: 0; + bottom: 0; + + border-left: 1.5px solid #ffffff; + border-right: 1.5px solid #ffffff; + border-bottom: 1.5px solid #ffffff; + + border-radius: 0 0 24px 24px; + + pointer-events: none; + + z-index: 5; +} + +.tagsRow { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 20px; +} + +.mockTag { + background-color: #f898d0; + color: white; + padding: 4px 12px; + border-radius: 16px; + font-size: 0.75rem; + font-weight: 600; +} + +.cardTitle { + margin: 0; + font-size: 1.25rem; + font-weight: 800; + color: #111827; + line-height: 1.3; +} + +.cardMeta { + font-size: 0.875rem; + color: #4b5563; + display: flex; + flex-direction: column; + gap: 4px; +} + +.cardMeta strong { + color: #000; + text-decoration: underline; +} + +.cardDescription { + font-size: 0.9rem; + line-height: 1.5; + color: #374151; + margin: 0; +} + +.cardLink { + font-size: 0.75rem; + font-weight: 700; + color: #000; + text-decoration: underline; + word-break: break-all; + margin-top: auto; +} + +/*make a longer body if required*/ +.layer3GradientBody { + position: relative; + z-index: 3; + + background-image: + url('../images/block_gradient.png'), + linear-gradient(180deg, #FFF3E4 0%, #FFD6E0 100%); + + background-size: + 100% auto, + 100% 100%; + + background-repeat: + no-repeat, + no-repeat; + + background-position: + top center, + center center; + + margin-top: -3px; + + padding: 24px; + display: flex; + flex-direction: column; + gap: 16px; + + border-radius: 0 0 24px 24px; + overflow: hidden; +} \ No newline at end of file diff --git a/src/pages/ResourcesPage/ui/ResourceCard.tsx b/src/pages/ResourcesPage/ui/ResourceCard.tsx new file mode 100644 index 0000000..f3e55cb --- /dev/null +++ b/src/pages/ResourcesPage/ui/ResourceCard.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import styles from './ResourceCard.module.css'; + +interface ResourceCardProps { + title: string; + author: string; + date: string; + description: string; + link: string; + tags: string[]; + imageUrl?: string; +} + +export const ResourceCard: React.FC = ({ + title, + author, + date, + description, + link, + tags, + imageUrl + }) => { + return ( +
+ +
+ {tags.map((tag, index) => ( + + {tag} + + ))} +
+ + {imageUrl && ( +
+ {title} +
+ )} + +

{title}

+ +
+

Created by: {author}

+

Created was: {date}

+
+ +

+ {description} +

+ + + {link} + + +
+ ); +}; \ No newline at end of file diff --git a/src/pages/ResourcesPage/ui/ResourcesPage.module.css b/src/pages/ResourcesPage/ui/ResourcesPage.module.css new file mode 100644 index 0000000..e1c1317 --- /dev/null +++ b/src/pages/ResourcesPage/ui/ResourcesPage.module.css @@ -0,0 +1,215 @@ + +.pageWrapper { + max-width: 1400px; + margin: 0 auto; + padding: 20px; +} + +.pageHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + margin-top: 20px; +} + +.pageTitle { + font-size: 2rem; + font-weight: 700; + color: #111827; + margin: 10px 0 0 0; +} + +.headerActions { + display: flex; + gap: 15px; +} + +.pageDivider { + border: none; + border-top: 2px solid #e5e7eb; + margin-bottom: 30px; +} + +.mainContent { + display: flex; + gap: 40px; + align-items: flex-start; +} + +.leftSidebar { + width: 250px; + flex-shrink: 0; +} + +.resourcesGrid { + /* --- normal grid if wanted --- */ + /* display: grid; */ + /* grid-template-columns: repeat(3, 1fr); */ + /* gap: 20px; */ + /* align-items: start; */ + + /* --- MASONRY --- */ + column-count: 3; + column-gap: 20px; + width: 100%; +} +.resourceCardPlaceholder { + height: 350px; + border-radius: 20px; + background: linear-gradient(180deg, #ffe4e6 0%, #ffedd5 100%); + padding: 20px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + color: #9ca3af; +} + + +.headerActions { + display: flex; + align-items: center; + gap: 16px; +} + +/*filters button */ +.filterBtn { + background-color: #e8e8e8; + color: #888888; + border: none; + border-radius: 50px; + padding: 12px 24px; + font-size: 1rem; + cursor: pointer; + transition: background-color 0.2s; +} + +.filterBtn:hover { + background-color: #d4d4d4; +} + +/* 2. Input Search */ +.searchInput { + background-color: #e8e8e8; + color: #333333; + border: none; + border-radius: 50px; + padding: 12px 45px 12px 24px; + font-size: 1rem; + outline: none; + width: 250px; + + + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23888888'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M21 21l-4.35-4.35M17 10a7 7 0 11-14 0 7 7 0 0114 0z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 16px center; + background-size: 20px; +} + +.searchInput::placeholder { + color: #888888; +} + +/* add resource button */ +.addResourceBtn { + background-color: #4a4a4a; + color: #ffffff; + border: none; + border-radius: 50px; + padding: 12px 32px; + font-size: 1rem; + font-weight: 700; + cursor: pointer; + text-transform: uppercase; + transition: background-color 0.2s; +} + +.addResourceBtn:hover { + background-color: #333333; +} + +/*sidebar and tags*/ + +.sidebarTitle { + font-size: 1.25rem; + font-weight: 700; + color: #111827; + margin: 0 0 16px 5px; +} + +.tagsContainer { + position: relative; + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + background: rgba(250, 103, 115, 0.15); + backdrop-filter: blur(30px); + border-radius: 30px; + box-shadow: 0px 6px 10px 0px rgba(254, 68, 68, 0.25); + padding: 24px; + overflow: hidden; +} + +.tagsContainer::before { + content: ""; + position: absolute; + inset: 0; + background-color: rgba(250, 103, 115, 0.2); + border-radius: inherit; + filter: blur(30px); + z-index: -1; + box-shadow: 0px 6px 10px 0px rgba(254, 68, 68, 0.25); +} + +.sidebarTag { + position: relative; + display: inline-block; + background-color: #fef7f8; + padding: 8px 16px; + min-width: 60px; + text-align: center; + border-radius: 100px; + z-index: 0; + + font-size: 0.85rem; + font-weight: 600; + color: #170103; + font-family: 'Montserrat', sans-serif; + cursor: pointer; + transition: transform 0.2s ease; +} + +.sidebarTag:hover { + transform: translateY(-2px); +} + +.sidebarTag::before { + content: ""; + position: absolute; + inset: 0; + padding: 1.5px; + border-radius: inherit; + background: linear-gradient( + 90deg, + rgba(255, 55, 127, 1) 0%, + rgba(255, 212, 130, 1) 85% + ); + mask: + linear-gradient(90deg, #fff 0%, #fff 100%) content-box, + linear-gradient(90deg, #fff 0%, #fff 100%); + -webkit-mask-composite: destination-out; + mask-composite: exclude; + z-index: -1; +} + +.lockedCardImage { + width: 100%; + display: block; + + /* --- MASONRY --- */ + margin-bottom: 20px; + break-inside: avoid; +} \ No newline at end of file diff --git a/src/pages/ResourcesPage/ui/ResourcesPage.tsx b/src/pages/ResourcesPage/ui/ResourcesPage.tsx new file mode 100644 index 0000000..40256a4 --- /dev/null +++ b/src/pages/ResourcesPage/ui/ResourcesPage.tsx @@ -0,0 +1,223 @@ +import { useEffect, useState, useRef, useCallback } from 'react'; +import styles from './ResourcesPage.module.css'; +import { useUser } from "../../../shared/lib/customHooks/useUser"; +import Navbar from "../../../shared/ui/Navbar/Navbar"; +import { ResourceCard } from "./ResourceCard.tsx"; +import lockedCardImg from "../images/lock card.png"; +import Footer from "../../../shared/ui/Footer/Footer.tsx"; + +// just to see if this works +const someResources = [ + { + id: 1, + title: "Figma UI Kit for Beginners", + author: "Mara", + date: "15 Jan 2026", + description: "Download this course and start learning UI from zero", + link: "https://figma.com/community/...", + tags: ["#Design", "#UI", "#Figma"], + imageUrl: "https://images.unsplash.com/photo-1618761714954-0b8cd0026356?q=80&w=500", + requiresAuth: false + }, + { + id: 2, + title: "Secret tutorial", + author: "Maria", + date: "20 Nov 2024", + description: "description here", + link: "", + tags: ["#Management", "#Internal"], + requiresAuth: true + }, + { + id: 3, + title: "TypeScript for Frontend", + author: "Andrei", + date: "22 Feb 2026", + description: "Discover how TypeScript bridges the gap between dynamic JavaScript and robust software engineering. This resource delves into integrating static typing with modern frontend workflows, and building scalable, maintainable user interfaces for the web.", + link: "https://typescriptlang.org", + tags: ["#TypeScript", "#Frontend"], + requiresAuth: false + }, + { + id: 4, + title: "Advanced React Patterns", + author: "Andrei", + date: "22 Feb 2026", + description: "Learn how to build reusable components.", + link: "https://react.dev", + tags: ["#React", "#Frontend"], + requiresAuth: false + }, + { + id: 5, + title: "CSS Grid & Flexbox Masterclass", + author: "Andrei", + date: "22 Feb 2026", + description: "Master layouts in the browser.", + link: "https://developer.mozilla.org", + tags: ["#CSS", "#Frontend"], + requiresAuth: false + }, + { + id: 6, + title: "Mentor Guidelines", + author: "Maria", + date: "20 Nov 2024", + description: "Internal documentation for mentors.", + link: "", + tags: ["#Management", "#Internal"], + requiresAuth: true + }, + { + id: 7, + title: "Onboarding Checklist", + author: "Maria", + date: "20 Nov 2024", + description: "Checklist for new team members.", + link: "", + tags: ["#Management", "#Internal"], + requiresAuth: true + } +]; + +export const ResourcesPage = () => { + const userIdFromLocalStorage = localStorage.getItem("userId") + ? Number(localStorage.getItem("userId")) + : null; + + const { user } = useUser(userIdFromLocalStorage); + + const [isAuth, setIsAuth] = useState(false); + const [isMentor, setIsMentor] = useState(false); + + // --- LOGIC FOR INFINITE SCROLL --- + const ITEMS_PER_PAGE = 3; // only 3 to test + const [visibleResources, setVisibleResources] = useState(someResources.slice(0, ITEMS_PER_PAGE)); + const [isLoading, setIsLoading] = useState(false); + const [hasMore, setHasMore] = useState(someResources.length > ITEMS_PER_PAGE); + + const observer = useRef(null); + const observerTarget = useCallback((node: HTMLDivElement) => { + if (isLoading) return; + if (observer.current) observer.current.disconnect(); + + observer.current = new IntersectionObserver(entries => { + if (entries[0].isIntersecting && hasMore) { + loadMoreResources(); + } + }); + + if (node) observer.current.observe(node); + }, [isLoading, hasMore]); + + const loadMoreResources = () => { + setIsLoading(true); + setTimeout(() => { + const currentLength = visibleResources.length; + const nextItems = someResources.slice(currentLength, currentLength + ITEMS_PER_PAGE); + + if (nextItems.length > 0) { + setVisibleResources(prev => [...prev, ...nextItems]); + if (currentLength + nextItems.length >= someResources.length) { + setHasMore(false); + } + } else { + setHasMore(false); + } + setIsLoading(false); + }, 1000); + }; + + useEffect(() => { + if (user) { + setIsMentor( + user?.roles?.filter( + (role: { role_id: number; role_name: string }) => + role.role_name === "mentor" + ).length > 0 + ); + setIsAuth(true); + } else { + setIsAuth(false); + setIsMentor(false); + } + }, [user]); + + return ( +
+ + +
+

Our resources

+ +
+ + + + {isMentor && ( + + )} +
+
+ +
+ +
+ + +
+
+ {/* iterate through visible resources NOT all resources (logic for infinite scroll) */} + {visibleResources.map((resource) => { + if (resource.requiresAuth && !isAuth) { + return ( + Available only to logged in users + ); + } + + return ( + + ); + })} +
+ +
+ {isLoading &&

Loading more resources...

} + {!hasMore &&

You have reached the end.

} +
+
+
+ +
+
+ ); +}; \ No newline at end of file diff --git a/src/shared/config/routeConfig/routeConfig.tsx b/src/shared/config/routeConfig/routeConfig.tsx index 99164fa..5a53f1a 100644 --- a/src/shared/config/routeConfig/routeConfig.tsx +++ b/src/shared/config/routeConfig/routeConfig.tsx @@ -20,6 +20,7 @@ import TandC from "../../../pages/TandC"; import { CreateApplicationMentorPage } from "../../../pages/CreateApplicationMentorPage"; import { MentorUserProfilePage } from "../../../pages/MentorUserProfilePage"; import { AuthDiscordCallbackPage } from "../../../pages/AuthDiscordCallbackPage"; +import { ResourcesPage } from "../../../pages/ResourcesPage"; export enum AppRoutes { MAIN = "main", @@ -42,6 +43,7 @@ export enum AppRoutes { CREATE_APPLICATION_MENTOR = "create_application_mentor", MENTOR_USER_PROFILE = "mentor_user_profile/:id", AUTH_DISCORD_CALLBACK = "auth/discord/callback", + RESOURCES_PAGE = "resources_page", } export const RoutePath: Record = { @@ -65,6 +67,7 @@ export const RoutePath: Record = { [AppRoutes.CREATE_APPLICATION_MENTOR]: "/create_application_mentor", [AppRoutes.MENTOR_USER_PROFILE]: "/mentor_user_profile/:id", [AppRoutes.AUTH_DISCORD_CALLBACK]: "/auth/discord/callback", + [AppRoutes.RESOURCES_PAGE]: "/resources_page", }; export const routeConfig: Record = { @@ -148,4 +151,8 @@ export const routeConfig: Record = { path: RoutePath[AppRoutes.AUTH_DISCORD_CALLBACK], element: , }, + [AppRoutes.RESOURCES_PAGE]:{ + path: RoutePath[AppRoutes.RESOURCES_PAGE], + element: + }, }; diff --git a/src/shared/ui/Footer/Footer.tsx b/src/shared/ui/Footer/Footer.tsx index ec94435..3dcc629 100644 --- a/src/shared/ui/Footer/Footer.tsx +++ b/src/shared/ui/Footer/Footer.tsx @@ -34,7 +34,7 @@ export default function Footer() { Study Buddy (Coming soon)
  • - Resources (Coming soon) + Resources
  • diff --git a/src/shared/ui/Navbar/Navbar.tsx b/src/shared/ui/Navbar/Navbar.tsx index f7f0790..8033e7d 100644 --- a/src/shared/ui/Navbar/Navbar.tsx +++ b/src/shared/ui/Navbar/Navbar.tsx @@ -89,6 +89,12 @@ export default function Navbar({ isLogin = false }: { isLogin?: boolean }) { > Home + + Resources +