diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/package-lock.json b/package-lock.json index 1931c18..3aa6e39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@fontsource/sacramento": "^5.2.6", "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.23.6", "lottie-react": "^2.4.1", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -3296,6 +3297,33 @@ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true }, + "node_modules/framer-motion": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.6.tgz", + "integrity": "sha512-dsJ389QImVE3lQvM8Mnk99/j8tiZDM/7706PCqvkQ8sSCnpmWxsgX+g0lj7r5OBVL0U36pIecCTBoIWcM2RuKw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.6", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -4601,6 +4629,21 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/motion-dom": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.6.tgz", + "integrity": "sha512-G2w6Nw7ZOVSzcQmsdLc0doMe64O/Sbuc2bVAbgMz6oP/6/pRStKRiVRV4bQfHp5AHYAKEGhEdVHTM+R3FDgi5w==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5475,8 +5518,7 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/type-check": { "version": "0.4.0", diff --git a/package.json b/package.json index 776582e..5834212 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "dependencies": { "@fontsource/sacramento": "^5.2.6", "embla-carousel-react": "^8.6.0", + "framer-motion": "^12.23.6", "lottie-react": "^2.4.1", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/src/App.jsx b/src/App.jsx index f01ad92..0445af4 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,13 +10,13 @@ import Preloader from "./pages/Preloader/Preloader"; import ProfilePage from "./pages/Profile/ProfilePage.jsx"; import PrivateRoutesWrapper from "./components/ProtectedRoutes/PrivateRoutesWrapper.jsx"; import { ToastContainer } from "react-toastify"; -import 'react-toastify/dist/ReactToastify.css' +import "react-toastify/dist/ReactToastify.css"; function App() { return ( <> - + }> } /> @@ -29,7 +29,6 @@ function App() { } /> - } /> diff --git a/src/assets/mobile-nav/Account.png b/src/assets/mobile-nav/Account.png new file mode 100644 index 0000000..0809aec Binary files /dev/null and b/src/assets/mobile-nav/Account.png differ diff --git a/src/assets/mobile-nav/YDOlogo.png b/src/assets/mobile-nav/YDOlogo.png new file mode 100644 index 0000000..e07040f Binary files /dev/null and b/src/assets/mobile-nav/YDOlogo.png differ diff --git a/src/assets/mobile-nav/close.svg b/src/assets/mobile-nav/close.svg new file mode 100644 index 0000000..0c2532d --- /dev/null +++ b/src/assets/mobile-nav/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/mobile-nav/hamburger.svg b/src/assets/mobile-nav/hamburger.svg new file mode 100644 index 0000000..6eb0513 --- /dev/null +++ b/src/assets/mobile-nav/hamburger.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/mobile-nav/notifications.png b/src/assets/mobile-nav/notifications.png new file mode 100644 index 0000000..ec6b009 Binary files /dev/null and b/src/assets/mobile-nav/notifications.png differ diff --git a/src/assets/mobile-nav/search.png b/src/assets/mobile-nav/search.png new file mode 100644 index 0000000..ee9661e Binary files /dev/null and b/src/assets/mobile-nav/search.png differ diff --git a/src/assets/mobile-nav/settings.png b/src/assets/mobile-nav/settings.png new file mode 100644 index 0000000..dfc1534 Binary files /dev/null and b/src/assets/mobile-nav/settings.png differ diff --git a/src/components/mobile-carousel/MobCarousel.css b/src/components/mobile-carousel/MobCarousel.css new file mode 100644 index 0000000..a65be6b --- /dev/null +++ b/src/components/mobile-carousel/MobCarousel.css @@ -0,0 +1,211 @@ +@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"); + +*, +::before, +::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +.card-stack-container { + position: relative; + width: 100%; + height: 600px; /* or however tall your cards are */ + display: grid; + place-items: center; +} + +.card-stack-item { + background: url("../../assets/Home/BG.png") no-repeat; + background-size: cover; + grid-row: 1; + grid-column: 1; + padding: 10px; + border-radius: 30px; + backdrop-filter: blur(45px); + background-color: rgba(0, 0, 0, 0.8); + width: 320px; + height: 600px; + border: 2px solid white; +} + +.card__image { + position: absolute; + margin: 5px 0; + width: 80%; + height: 51%; + left: 50%; + transform: translateX(-50%); + z-index: -1; +} + +.card__image img { + display: block; + width: 100%; + height: 100%; + border-radius: 20px; +} + +.card__data { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 200px; + max-height: 500px; +} + +.head-wrapper { + background-color: rgba(0, 0, 0, 0.3); + backdrop-filter: blur(25px); + border-radius: 20px; +} + +.name-age { + font-family: "Poppins", sans-serif; + font-size: 30px; + font-weight: 700; + color: rgba(246, 204, 182, 1); + padding: 1px 2px; +} + +.discipline-year { + font-family: "Poppins", sans-serif; + font-size: 24px; + font-weight: 400; + color: rgba(246, 204, 182, 0.9); + padding: 1px 10px; +} + +.tags { + display: flex; + flex-flow: row wrap; + gap: 0.8rem; + justify-content: center; +} + +.tag { + font-family: "Poppins", sans-serif; + background-color: rgba(246, 204, 182, 0.2); + border: 1px solid white; + border-radius: 12px; + padding: 0.2rem 0.6rem; + font-size: 0.8rem; +} + +.description { + font-family: "Poppins", sans-serif; + background: rgba(246, 204, 182, 0.2); + border-radius: 20px; + padding: 0 8px; + border: 1px solid white; + max-height: 100px; + overflow: auto; +} + +.user-card-wrapper { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.description p { + padding: 5px; + font-size: 14px; +} + +.skeleton-card { + width: 100%; + height: 100%; + position: relative; + display: flex; + flex-direction: column; + gap: 0.6rem; + align-items: center; + border-radius: 20px; + padding: 10px; + background: url("../../assets/Home/BG.png") no-repeat; + background-size: cover; + backdrop-filter: blur(45px); +} + +.error { + position: absolute; + top: 50%; +} + +.skeleton-image { + width: 100%; + height: 70%; + background: rgba(246, 204, 182, 0.2); + border-radius: 20px; +} + +.skeleton-data { + padding: 20px; + flex: 1; +} + +.skeleton-line { + background: rgba(0, 0, 0, 0.3); + border-radius: 4px; + margin-bottom: 12px; +} + +.skeleton-name { + height: 24px; + width: 60%; +} + +.skeleton-discipline { + height: 18px; + width: 45%; +} + +.skeleton-tags { + display: flex; + gap: 8px; + margin: 16px 0; +} + +.skeleton-tag { + height: 20px; + width: 50px; + background: #e2e5e7; + border-radius: 12px; +} + +.skeleton-description { + margin-top: 16px; +} + +.skeleton-desc-line { + height: 16px; + width: 100%; + margin-bottom: 8px; +} + +.skeleton-desc-short { + width: 70%; +} + +.shimmer { + background: linear-gradient( + 90deg, + rgba(246, 204, 182, 0.2) 25%, + rgba(246, 204, 182, 0.4) 50%, + rgba(246, 204, 182, 0.2) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} diff --git a/src/components/mobile-carousel/MobCarousel.jsx b/src/components/mobile-carousel/MobCarousel.jsx new file mode 100644 index 0000000..a0db6b5 --- /dev/null +++ b/src/components/mobile-carousel/MobCarousel.jsx @@ -0,0 +1,209 @@ +import React, { useState, useEffect } from "react"; +import { + motion, + AnimatePresence, + useMotionValue, + useTransform, +} from "framer-motion"; +import fetchUsers from "../../utils/fetchProfiles"; +import "./MobCarousel.css"; + +const SkeletonCard = () => ( + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+); + +const UserCard = ({ + name, + age, + discipline, + year, + tags, + image, + description, + profiles, + id, + onSwipe, +}) => { + const x = useMotionValue(0); + + const opacity = useTransform(x, [-150, 0, 150], [0, 1, 0]); + const rotateRaw = useTransform(x, [-150, 150], [-18, 18]); + const isFront = id === profiles[profiles.length - 1].id; + const rotate = useTransform(() => { + const offSet = isFront ? 0 : id % 2 ? 6 : -6; + return `${rotateRaw.get() + offSet}deg`; + }); + + const handleDragEnd = () => { + const swipe = x.get(); + if (Math.abs(swipe) > 100) { + const profile = profiles.find((p) => p.id === id); + const direction = swipe > 0 ? "right" : "left"; + onSwipe(profile, direction); + } + }; + return ( + + +
+ user-image +
+
+
+

+ {name} {age} +

+

+ {discipline} {year} +

+
+
+ {tags.map((tag, index) => ( +
+ {tag} +
+ ))} +
+
+

{description}

+
+
+
+
+ ); +}; + +export default function MobCarousel() { + const [profiles, setProfiles] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [liked, setLiked] = useState([]); + const [disliked, setDisliked] = useState([]); + + useEffect(() => { + const controller = new AbortController(); + const loadProfiles = async () => { + try { + setLoading(true); + const { data, error } = await fetchUsers(controller.signal); + if (error) throw new Error(error); + setProfiles(data); + } catch (error) { + setError("Could not load profiles"); + console.error("Failed to fetch profiles:", error); + } finally { + setLoading(false); + } + }; + loadProfiles(); + return () => controller.abort(); + }, []); + + const handleSwipe = (profile, direction) => { + if (direction === "right") { + setLiked((prev) => [...prev, profile]); + } else { + setDisliked((prev) => [...prev, profile]); + } + setProfiles((prev) => prev.filter((p) => p.id !== profile.id)); + }; + + //test stuff + useEffect(() => { + console.log("liked:", liked); + console.log("disliked:", disliked); + }, [liked, disliked]); + + if (loading) { + return ( + +
+
+ +
+
+
+ ); + } + + if (error) { + return ( +
+
{error}
+
+ ); + } + + if (profiles.lenth === 0) { + return ( +
+

No more profiles 💔

+ +
+ ); + } + + return ( +
+ {profiles.map((profile) => ( + + + + ))} +
+ ); +} diff --git a/src/components/mobile-footer/MobFoot.css b/src/components/mobile-footer/MobFoot.css new file mode 100644 index 0000000..72a6210 --- /dev/null +++ b/src/components/mobile-footer/MobFoot.css @@ -0,0 +1,9 @@ +.mobile-footer { + width: 100vw; + position: relative; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 50px; + bottom: 10px; +} diff --git a/src/components/mobile-footer/MobFoot.jsx b/src/components/mobile-footer/MobFoot.jsx new file mode 100644 index 0000000..6d90c6c --- /dev/null +++ b/src/components/mobile-footer/MobFoot.jsx @@ -0,0 +1,29 @@ +import { Link } from "react-router-dom"; +import notifications from "../../assets/mobile-nav/notifications.png"; +import settings from "../../assets/mobile-nav/settings.png"; +import search from "../../assets/mobile-nav/search.png"; +import "./MobFoot.css"; + +export default function MobFoot() { + return ( + <> +
+
+ + notificaations + +
+
+ + settings + +
+
+ + search + +
+
+ + ); +} diff --git a/src/components/mobile-nav/MobNav.css b/src/components/mobile-nav/MobNav.css new file mode 100644 index 0000000..1b145e5 --- /dev/null +++ b/src/components/mobile-nav/MobNav.css @@ -0,0 +1,193 @@ +@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap"); + +.navbar { + position: fixed; + top: 0; + width: 100vw; + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 40px; + background: rgba(255, 255, 255, 0.02); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + z-index: 100; + height: 110px; +} + +.navbar__logo img, +.navbar__account img { + display: inline-block; + width: 100%; +} + +.navbar__logo { + height: auto; + width: 110px; +} + +.navbar__account { + height: auto; + width: 30px; +} + +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + z-index: 1000; + animation: fadeIn 0.8s ease-out; +} + +.overlay.closed { + animation: fadeOut 0.8s ease-out; +} + +.drawer { + position: fixed; + top: 0; + left: 0; + min-height: 100vh; + width: 320px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-right: 1px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); + animation: slideIn 0.8s ease-out; + font-family: "Poppins", sans-serif; +} + +.drawer.closed { + animation: slideOut 0.8s ease-out; +} + +.drawer__close { + display: flex; + justify-content: flex-start; + padding: 24px; +} + +.drawer__close img { + width: 24px; + height: 24px; +} + +.drawer__items { + padding: 0 32px; + margin-top: 32px; +} + +.drawer__items a { + display: block; + padding: 16px 20px; + text-decoration: none; + border-radius: 12px; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.drawer__text { + position: relative; + display: inline-block; +} + +.drawer__text::after { + content: ""; + position: absolute; + bottom: -4px; + left: 0; + width: 0; + height: 2px; + background-color: rgba(246, 204, 182, 1); + transition: width 0.3s ease; +} + +.drawer__items a:hover .drawer__text::after { + width: 100%; +} + +.drawer__text { + color: rgba(246, 204, 182, 1); + font-size: 25px; + font-weight: 500; + margin: 0; + font-family: "Poppins", sans-serif; + transition: all 0.2s ease; +} + +.drawer__items a:hover .drawer__text { + color: rgba(246, 204, 182, 1); + font-weight: 600; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideIn { + from { + transform: translateX(-100%); + } + to { + transform: translateX(0); + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + } + to { + transform: translateX(-100%); + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +.drawer::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 128px; + background: linear-gradient(to top, rgba(255, 255, 255, 0.05), transparent); + pointer-events: none; +} + +@media (max-width: 375px) { + .glassy-drawer { + width: 280px; + } +} + +.drawer { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15) 0%, + rgba(255, 255, 255, 0.05) 100% + ), + rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.18); + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37), + inset 0 1px 0 rgba(255, 255, 255, 0.2); +} diff --git a/src/components/mobile-nav/MobNav.jsx b/src/components/mobile-nav/MobNav.jsx new file mode 100644 index 0000000..7bc5255 --- /dev/null +++ b/src/components/mobile-nav/MobNav.jsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; +import Account from "../../assets/mobile-nav/Account.png"; +import YDOlogo from "../../assets/mobile-nav/YDOlogo.png"; +import hamburger from "../../assets/mobile-nav/hamburger.svg"; +import close from "../../assets/mobile-nav/close.svg"; +import "./MobNav.css"; + +export default function MobNav() { + const [isOpen, setIsOpen] = useState(false); + const [isClosing, setIsClosing] = useState(false); + + const toggleMenu = () => { + if (isOpen) { + setIsClosing(true); + setTimeout(() => { + setIsOpen(false); + setIsClosing(false); + }, 800); + } else { + setIsOpen(true); + } + }; + + const closeMenu = () => setIsOpen(false); + + return ( + <> +
+
+ menu +
+
+ logo +
+
+ account +
+
+ + {isOpen && ( +
+
e.stopPropagation()} + > +
+ close +
+
+ +

Home

+ + +

Choice

+ + +

Matched

+ + +

Profile

+ +
+
+
+ )} + + ); +} diff --git a/src/pages/Home/Home.css b/src/pages/Home/Home.css index f011217..bf62289 100644 --- a/src/pages/Home/Home.css +++ b/src/pages/Home/Home.css @@ -12,6 +12,16 @@ body { display: flex; flex-direction: column; gap: 1rem; + min-height: 100vh; + overflow-y: auto; +} + +.mobile { + display: none; +} + +.desktop { + display: block; } .main { @@ -98,3 +108,76 @@ body { transition: all 0.4s ease; } } + +@media (max-width: 767px) { + .desktop { + display: none; + } + + .nav-header { + display: none; + } + + .text-wrapper--mobile { + width: 100vw; + margin-top: 7rem; + margin-bottom: 4rem; + } + + .text--mobile { + font-family: "Sacramento", cursive; + font-size: 3rem; + text-align: center; + color: rgba(246, 204, 182, 1); + letter-spacing: 1px; + } + + .mobile { + display: flex; + flex-direction: column; + width: 100vw; + min-height: 100vh; + overflow-y: auto; + overflow-x: hidden; + } + + .main--mobile { + flex: 1; + } + + .icons--mob { + display: flex; + justify-content: space-evenly; + align-items: center; + width: 80vw; + margin: 3rem auto; + padding: 1rem; + } + + .pass-icon--mob, + .smash--mob, + .refresh--mob { + width: 50px; + height: 50px; + } + + .pass-icon__image--mob, + .smash__image--mob { + width: 50px; + height: 50px; + transition: all 0.4s ease; + } + + .refresh__image--mob { + width: 50px; + height: 50px; + transition: all 0.4s ease; + } + + .pass-icon__image--mob:active, + .smash__image--mob:active, + .refresh__image--mob:active { + transform: scale(1.2); + transform: translateY(5px); + } +} diff --git a/src/pages/Home/Home.jsx b/src/pages/Home/Home.jsx index 9e1fdf1..999e5f7 100644 --- a/src/pages/Home/Home.jsx +++ b/src/pages/Home/Home.jsx @@ -4,26 +4,59 @@ import refreshIcon from "../../assets/Home/refreshIcon.png"; import heartIcon from "../../assets/Home/heartIcon.png"; import pass from "../../assets/Home/pass.png"; import EmblaCarousel from "../../components/infinite-carousel/EmblaCarousel"; +import MobNav from "../../components/mobile-nav/MobNav.jsx"; +import MobFoot from "../../components/mobile-footer/MobFoot.jsx"; +import MobCarousel from "../../components/mobile-carousel/MobCarousel.jsx"; import "@fontsource/sacramento"; const Home = () => { return ( <> - -
-
-

Ready to make someone blush today?🥰

-
- -
-
- pass +
+ +
+
+

+ Ready to make
someone +
blush today?🥰 +

+
+ +
+
+ pass +
+
+ smash +
+
+ refresh +
-
- smash +
+ +
+
+ +
+
+

Ready to make someone blush today?🥰

-
- refresh + +
+
+ pass +
+
+ smash +
+
+ refresh +