diff --git a/app/assets/images/github-icon.png b/app/assets/images/github-icon.png new file mode 100644 index 0000000..9490ffc Binary files /dev/null and b/app/assets/images/github-icon.png differ diff --git a/app/assets/images/school-logo.svg b/app/assets/images/school-logo.svg new file mode 100644 index 0000000..324df31 --- /dev/null +++ b/app/assets/images/school-logo.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/pages/about/AboutText.tsx b/app/pages/about/AboutText.tsx new file mode 100644 index 0000000..4c61fd4 --- /dev/null +++ b/app/pages/about/AboutText.tsx @@ -0,0 +1,15 @@ +import { type ReactElement } from 'react' +import { H1, P } from '~/components/ui/typography' + +export function AboutText({ text }: { text: string }): ReactElement { + return ( +
+

About us

+ {text.split('\n\n').map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +
+ ) +} diff --git a/app/pages/about/GithubLink.tsx b/app/pages/about/GithubLink.tsx new file mode 100644 index 0000000..9c88176 --- /dev/null +++ b/app/pages/about/GithubLink.tsx @@ -0,0 +1,11 @@ +import type { ReactElement } from 'react' +import GithubIcon from '~/assets/images/github-icon.png' + +export function GithubLink({ url }: { url: string }): ReactElement { + return ( + + GitHub + {url.split('/').pop()} + + ) +} diff --git a/app/pages/about/InfoLine.tsx b/app/pages/about/InfoLine.tsx new file mode 100644 index 0000000..6bcbe84 --- /dev/null +++ b/app/pages/about/InfoLine.tsx @@ -0,0 +1,11 @@ +import type { ReactElement } from 'react' +import { P } from '~/components/ui/typography' + +export function InfoLine({ label, text }: { label: string; text?: string }): ReactElement | undefined { + if (!text) return undefined + return ( +

+ {label}: {text} +

+ ) +} diff --git a/app/pages/about/SchoolLogo.tsx b/app/pages/about/SchoolLogo.tsx new file mode 100644 index 0000000..6860814 --- /dev/null +++ b/app/pages/about/SchoolLogo.tsx @@ -0,0 +1,12 @@ +import { type ReactElement } from 'react' +import SchoolIcon from '~/assets/images/school-logo.svg' + +export function SchoolLogo(): ReactElement { + return ( +
+ + RSSchool logo + +
+ ) +} diff --git a/app/pages/about/TeamMember.tsx b/app/pages/about/TeamMember.tsx new file mode 100644 index 0000000..bfc1557 --- /dev/null +++ b/app/pages/about/TeamMember.tsx @@ -0,0 +1,44 @@ +import { type ReactElement } from 'react' +import { H2 } from '~/components/ui/typography' +import { GithubLink } from './GithubLink' +import { InfoLine } from './InfoLine' + +interface TeamMemberProperties { + name: string + location?: string + education?: string + courses: string + about: string + github: string + imageUrl: string +} + +export function TeamMember({ + name, + location, + education, + courses, + about, + github, + imageUrl +}: TeamMemberProperties): ReactElement { + return ( +
+ {name} +
+

{name}

+ + + + + +
+
+ ) +} diff --git a/app/pages/about/aboutData.tsx b/app/pages/about/aboutData.tsx new file mode 100644 index 0000000..0cd5391 --- /dev/null +++ b/app/pages/about/aboutData.tsx @@ -0,0 +1,38 @@ +export const teamMembers = [ + { + name: 'MIKHAIL', + courses: 'RSSchool JavaScript/Front-end, NodeJS', + about: + "I'm a frontend developer with a background in full-stack development, currently sharpening my JavaScript skills through RS School. I'm passionate about frontend technologies, enjoy exploring frameworks, and enjoy learning through community-driven collaboration.", + github: 'https://github.com/bitbybit', + imageUrl: 'https://live.staticflickr.com/65535/54588004201_88d9183d76_c.jpg', + reverse: false + }, + { + name: 'MERU', + location: 'Aktau, Kazakhstan', + education: 'Al-Farabi Kazakh National University, accounting and auditing', + courses: 'RSSchool JavaScript/Front-end Pre-school, JavaScript/Front-end, NodeJS', + about: + 'Wherever I worked before, I always felt drawn to programming. To pursue this goal, I’m currently finishing a course at RS School. Through tons of practice and great learning projects, I’m gaining the experience I need to become a frontend developer. I use Javascript, Typescript, React, and Node.js in my projects.', + github: 'https://github.com/merucoding', + imageUrl: 'https://raw.githubusercontent.com/merucoding/school-project-pictures/main/IMG_1996_1.jpeg', + reverse: true + }, + { + name: 'KATE', + location: 'Tbilisi, Georgia', + education: 'Samara University. Applied mathematics and computer science', + courses: 'RSSchool JavaScript/Front-end', + about: + "I'm deeply interested in frontend development and love the process of turning ideas into interactive, user-friendly interfaces. Through RSSchool, I’ve been actively improving my skills in JavaScript, TypeScript, and React - and I genuinely enjoy exploring how all the parts of a web app come together.", + github: 'https://github.com/zhuravel17', + imageUrl: 'https://raw.github.com/zhuravel17/rsschool-cv/main/kate.jpg', + reverse: false + } +] +export const aboutUsText = `Our team put a lot of effort into building this final project, working together in a coordinated and thoughtful way. Each team member made a significant contribution, taking ownership of key parts of the application and actively participating in the collaborative development process. + +Mikhail laid the technical foundation of the project from the start: he set up the repository, organized the task board, and ensured a solid structure for our teamwork. He developed the registration page and user profile page, and also worked on the cart page together with Meru. In addition to his development work, Mikhail shared his experience through code reviews — his feedback and suggestions helped improve code quality and deepen our understanding of the implementation. Meru focused on key user-facing pages — she implemented the login page and the product catalog. Together with Mikhail, she also contributed to the development of the cart page, ensuring both its functionality and consistency with the rest of the UI. Kate worked on the overall interface and layout. She created the header and footer, the main page, the detailed product page, and the “About Us” page. Her contribution brought coherence, accessibility, and a polished visual style to the application. + +Each of us not only worked on specific components but also actively participated in discussions, testing, and refinement. As a result, we were able to deliver a cohesive, functional, and well-crafted web product.` diff --git a/app/pages/about/index.tsx b/app/pages/about/index.tsx index 6fcfd6d..cbd890f 100644 --- a/app/pages/about/index.tsx +++ b/app/pages/about/index.tsx @@ -1,8 +1,24 @@ import { type ReactElement } from 'react' import { useTitle } from '~/hooks/useTitle' +import { teamMembers, aboutUsText } from './aboutData' +import { AboutText } from './AboutText' +import { SchoolLogo } from './SchoolLogo' +import { TeamMember } from './TeamMember' -export default function Routes(): ReactElement { +export default function About(): ReactElement { useTitle('About') - return <>About + return ( +
+ +
+ {teamMembers.map((member) => ( +
+ +
+ ))} +
+ +
+ ) } diff --git a/app/pages/product/ProductDetail/CartToggleButton.tsx b/app/pages/product/ProductDetail/CartToggleButton.tsx new file mode 100644 index 0000000..ef0164b --- /dev/null +++ b/app/pages/product/ProductDetail/CartToggleButton.tsx @@ -0,0 +1,50 @@ +import { type ReactElement, useState, type MouseEvent } from 'react' +import { type ProductProjection } from '@commercetools/platform-sdk' +import { useAppDispatch, useAppSelector } from '~/store/hooks' +import { addProduct, removeProduct, selectIsInCart } from '~/store/cart' +import { Button } from '~/components/ui/Button' +import { toast } from 'sonner' +import { P } from '~/components/ui/typography' + +type CartToggleButtonProperties = { product: ProductProjection } + +export function CartToggleButton({ product }: CartToggleButtonProperties): ReactElement { + const dispatch = useAppDispatch() + const isInCart = useAppSelector(selectIsInCart(product)) + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState() + + const handleClick = async (event: MouseEvent): Promise => { + event.stopPropagation() + setIsLoading(true) + setError(undefined) + try { + if (isInCart) { + await dispatch(removeProduct({ productId: product.id, quantity: 1 })).unwrap() + toast('❌ Removed from Cart') + } else { + await dispatch(addProduct({ productId: product.id, quantity: 1 })).unwrap() + toast('🛒 Added to Cart') + } + } catch (error) { + setError('An error occurred. Please try again.') + toast(error instanceof Error ? error.message : '❌ Failed to update the cart') + } finally { + setIsLoading(false) + } + } + + return ( + <> + + {error &&

{error}

} + + ) +} diff --git a/app/pages/product/ProductDetail/ProductDetailBody.tsx b/app/pages/product/ProductDetail/ProductDetailBody.tsx index e322867..09b73f6 100644 --- a/app/pages/product/ProductDetail/ProductDetailBody.tsx +++ b/app/pages/product/ProductDetail/ProductDetailBody.tsx @@ -5,27 +5,7 @@ import { ProductImages } from './ProductImages' import { ProductInfo } from './ProductInfo' import { ProductBreadcrumbs } from './ProductBreadcrumbs' import { type ProductListCategory } from '~/api/namespaces/product' - -function getBreadcrumbs( - categories: ProductListCategory[], - categoryId: string, - breadcrumbs: ProductListCategory[] = [] -): ProductListCategory[] { - for (const category of categories) { - const newBreadcrumbs = [...breadcrumbs, category] - - if (category.id === categoryId) { - return newBreadcrumbs - } - - if (category.subCategories !== undefined && category.subCategories.length > 0) { - const result = getBreadcrumbs(category.subCategories, categoryId, newBreadcrumbs) - if (result) return result - } - } - - return [] -} +import { useProductInfo } from '../hooks/useProductInfo' export function ProductDetailBody({ categories, @@ -38,13 +18,7 @@ export function ProductDetailBody({ throw new Error('Product not found') } - const name = product.name - const description = product.description ?? name - const price = product.masterVariant.prices?.[0]?.value?.centAmount ?? 0 - const images = product.masterVariant.images ?? [] - const discount = product.masterVariant.prices?.[0].discounted?.value?.centAmount ?? undefined - const breadcrumbs = - product.categories?.[0]?.id === undefined ? [] : getBreadcrumbs(categories, product.categories[0].id) + const { breadcrumbs, images } = useProductInfo(product, categories) return (
@@ -52,7 +26,7 @@ export function ProductDetailBody({ - +
diff --git a/app/pages/product/ProductDetail/ProductImages.tsx b/app/pages/product/ProductDetail/ProductImages.tsx index 4f74853..6805117 100644 --- a/app/pages/product/ProductDetail/ProductImages.tsx +++ b/app/pages/product/ProductDetail/ProductImages.tsx @@ -15,7 +15,7 @@ export function ProductImages({ images = [] }: { images?: Image[] }): ReactEleme diff --git a/app/pages/product/ProductDetail/ProductInfo.tsx b/app/pages/product/ProductDetail/ProductInfo.tsx index 3c3faca..b3c3594 100644 --- a/app/pages/product/ProductDetail/ProductInfo.tsx +++ b/app/pages/product/ProductDetail/ProductInfo.tsx @@ -1,21 +1,14 @@ -import { type LocalizedString } from '@commercetools/platform-sdk' +import { type ProductProjection } from '@commercetools/platform-sdk' import { type ReactElement } from 'react' import { H1, H3, P } from '~/components/ui/typography' import { ProductPrice } from '~/components/product/ProductPrice' +import { CartToggleButton } from './CartToggleButton' +import { useProductInfo } from '../hooks/useProductInfo' const LANG = 'en-US' -export function ProductInfo({ - name, - description, - price, - discount -}: { - name: LocalizedString - description: LocalizedString - price: number - discount?: number -}): ReactElement { +export function ProductInfo({ product }: { product: ProductProjection }): ReactElement { + const { name, description, price, discount } = useProductInfo(product) return (

{name[LANG]}

@@ -23,6 +16,7 @@ export function ProductInfo({

+
) } diff --git a/app/pages/product/hooks/useProductInfo.tsx b/app/pages/product/hooks/useProductInfo.tsx new file mode 100644 index 0000000..e273810 --- /dev/null +++ b/app/pages/product/hooks/useProductInfo.tsx @@ -0,0 +1,52 @@ +import { type ProductProjection } from '@commercetools/platform-sdk' +import { type ProductListCategory } from '~/api/namespaces/product' + +type ProductInfoResult = { + name: ProductProjection['name'] + description: ProductProjection['description'] | ProductProjection['name'] + price: number + discount?: number + images: NonNullable + breadcrumbs: ProductListCategory[] +} + +export function getBreadcrumbs( + categories: ProductListCategory[], + categoryId: string, + breadcrumbs: ProductListCategory[] = [] +): ProductListCategory[] { + for (const category of categories) { + const newBreadcrumbs = [...breadcrumbs, category] + + if (category.id === categoryId) { + return newBreadcrumbs + } + + if (category.subCategories !== undefined && category.subCategories.length > 0) { + const result = getBreadcrumbs(category.subCategories, categoryId, newBreadcrumbs) + if (result) return result + } + } + + return [] +} + +export function useProductInfo(product: ProductProjection, categories?: ProductListCategory[]): ProductInfoResult { + const name = product.name + const description = product.description ?? name + const price = product.masterVariant.prices?.[0]?.value?.centAmount ?? 0 + const discount = product.masterVariant.prices?.[0]?.discounted?.value?.centAmount + const images = product.masterVariant.images ?? [] + + const breadcrumbs = + categories && product.categories?.[0]?.id ? getBreadcrumbs(categories, product.categories[0].id) : [] + + return { + name, + description, + price, + discount, + images, + breadcrumbs + } +}