From 68bff5ddf1acbf0cc66d04fe0452bff290e7ce8c Mon Sep 17 00:00:00 2001 From: Someone Account <158297903+Yenot52@users.noreply.github.com> Date: Sat, 15 Nov 2025 17:34:58 +0200 Subject: [PATCH 1/2] frontend: new-participants-grid-107 - version 2 --- frontend/jest.config.cjs | 12 +-- frontend/package-lock.json | 33 ++++++- frontend/package.json | 4 +- frontend/src/components/Hero/Hero.scss | 71 +++++++--------- .../NewParticipants/StartupCard.tsx | 33 +++++++ .../NewParticipants/StartupsGrid.scss | 82 ++++++++++++++++++ .../NewParticipants/StartupsGrid.tsx | 85 +++++++++++++++++++ .../__tests__/StartupsGrid.test.tsx | 21 +++++ .../src/components/NewParticipants/types.ts | 9 ++ .../components/WhyWorthGrid/WhyWorthGrid.scss | 8 ++ frontend/src/pages/Home.tsx | 2 + frontend/src/setupTests.ts | 2 + frontend/tsconfig.app.json | 20 +++-- 13 files changed, 321 insertions(+), 61 deletions(-) create mode 100644 frontend/src/components/NewParticipants/StartupCard.tsx create mode 100644 frontend/src/components/NewParticipants/StartupsGrid.scss create mode 100644 frontend/src/components/NewParticipants/StartupsGrid.tsx create mode 100644 frontend/src/components/NewParticipants/__tests__/StartupsGrid.test.tsx create mode 100644 frontend/src/components/NewParticipants/types.ts diff --git a/frontend/jest.config.cjs b/frontend/jest.config.cjs index 60de0f9..3d2f855 100644 --- a/frontend/jest.config.cjs +++ b/frontend/jest.config.cjs @@ -2,17 +2,11 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom', setupFilesAfterEnv: ['/src/setupTests.ts'], + transform: { + '^.+\\.tsx?$': ['ts-jest', { tsconfig: '/tsconfig.app.json' }] + }, moduleNameMapper: { '\\.(css|scss)$': 'identity-obj-proxy', '\\.(jpg|jpeg|png|gif|svg)$': '/src/mocks/fileMock.js' - }, - globals: { - 'ts-jest': { - tsconfig: '/tsconfig.app.json', - isolatedModules: true - } - }, - moduleNameMapper: { - '\\.(css|scss)$': 'identity-obj-proxy' } }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b9bc350..2ae353a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -34,6 +34,7 @@ "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-css-modules-transform": "^4.4.2", "jest-environment-jsdom": "^30.2.0", @@ -43,7 +44,8 @@ "ts-jest": "^29.4.5", "typescript": "~5.9.3", "typescript-eslint": "^8.46.1", - "vite": "npm:rolldown-vite@7.1.14" + "vite": "npm:rolldown-vite@7.1.14", + "whatwg-fetch": "^3.6.20" } }, "node_modules/@adobe/css-tools": { @@ -1660,6 +1662,8 @@ }, "node_modules/@types/jest": { "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", "dev": true, "license": "MIT", "dependencies": { @@ -3469,6 +3473,13 @@ "uglify-js": "^3.1.4" } }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true, + "license": "(Apache-2.0 OR MPL-1.1)" + }, "node_modules/has-flag": { "version": "4.0.0", "dev": true, @@ -3597,6 +3608,19 @@ "node": ">=0.10.0" } }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "license": "MIT", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -6744,6 +6768,13 @@ "node": ">=18" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true, + "license": "MIT" + }, "node_modules/whatwg-mimetype": { "version": "4.0.0", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index a27e88f..2df9b08 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,7 @@ "eslint-plugin-react-hooks": "^7.0.0", "eslint-plugin-react-refresh": "^0.4.22", "globals": "^16.4.0", + "identity-obj-proxy": "^3.0.0", "jest": "^30.2.0", "jest-css-modules-transform": "^4.4.2", "jest-environment-jsdom": "^30.2.0", @@ -46,7 +47,8 @@ "ts-jest": "^29.4.5", "typescript": "~5.9.3", "typescript-eslint": "^8.46.1", - "vite": "npm:rolldown-vite@7.1.14" + "vite": "npm:rolldown-vite@7.1.14", + "whatwg-fetch": "^3.6.20" }, "overrides": { "vite": "npm:rolldown-vite@7.1.14" diff --git a/frontend/src/components/Hero/Hero.scss b/frontend/src/components/Hero/Hero.scss index f445c17..b9d9152 100644 --- a/frontend/src/components/Hero/Hero.scss +++ b/frontend/src/components/Hero/Hero.scss @@ -2,7 +2,6 @@ $primary-green: #2b8a3e; $light-green: #b4d27a; $shadow-color: rgba(0, 0, 0, 0.2); - @font-face { font-family: 'Geologica'; src: url('../../fonts/Geologica-Regular.woff2') format('woff2'), @@ -11,7 +10,6 @@ $shadow-color: rgba(0, 0, 0, 0.2); font-style: normal; } - @font-face { font-family: 'Geologica'; src: url('../../fonts/Geologica-SemiBold.woff2') format('woff2'), @@ -20,7 +18,6 @@ $shadow-color: rgba(0, 0, 0, 0.2); font-style: normal; } - @font-face { font-family: 'Geologica'; src: url('../../fonts/Geologica-Bold.woff2') format('woff2'), @@ -29,22 +26,21 @@ $shadow-color: rgba(0, 0, 0, 0.2); font-style: normal; } - .hero-section { - background-color: $light-green; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - min-width: 345px; - height: 549px; - padding: 40px 24px; - box-sizing: border-box; + background-color: $light-green; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + min-width: 345px; + height: 720px; + padding: 40px 24px; + box-sizing: border-box; .content-container { display: flex; justify-content: space-between; - align-items: flex-start; + align-items: center; width: 100%; max-width: 1512px; padding: 0 40px; @@ -55,15 +51,16 @@ $shadow-color: rgba(0, 0, 0, 0.2); @media (min-width: 2100px) { max-width: 2000px; - } - } + } + } .text-column { flex: 1; max-width: 600px; - padding-right: 40px; - margin-top: 100px; - + display: flex; + flex-direction: column; + justify-content: center; + gap: 20px; .title { font-family: 'Geologica'; @@ -71,30 +68,25 @@ $shadow-color: rgba(0, 0, 0, 0.2); font-size: 64px; line-height: 120%; letter-spacing: 1%; - margin-bottom: 10px; - margin-top: 100px; + margin: 0; } - .subtitle { font-family: 'Geologica'; font-weight: 700; font-size: 32px; line-height: 120%; letter-spacing: 1%; - margin-bottom: 30px; - margin-top: 20px; + margin: 0; color: #333; } - .cta-button { width: 194px; white-space: nowrap; background-color: #000; color: #fff; - padding: 30px 1.5rem; - padding-top: 4px; + padding: 16px 24px; border: none; border-radius: 6px; font-family: 'Geologica'; @@ -102,6 +94,9 @@ $shadow-color: rgba(0, 0, 0, 0.2); font-size: 16px; cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; &:hover { background-color: #111; @@ -109,13 +104,11 @@ $shadow-color: rgba(0, 0, 0, 0.2); } } - .image-collage { position: relative; width: 700px; height: 500px; - .image-wrapper { position: absolute; border-radius: 8px; @@ -123,7 +116,6 @@ $shadow-color: rgba(0, 0, 0, 0.2); box-shadow: 0 10px 30px $shadow-color; background-color: white; - .collage-image { width: 100%; height: 100%; @@ -131,7 +123,6 @@ $shadow-color: rgba(0, 0, 0, 0.2); display: block; } - .image-label { font-family: 'Geologica'; font-weight: 600; @@ -148,34 +139,33 @@ $shadow-color: rgba(0, 0, 0, 0.2); } } - .winemaking { - top: 60px; - left: -250px; + top: 40px; + left: -250px; width: 300px; height: 400px; z-index: 2; } .delivery { - top: 20px; - left: 130px; + top: -40px; + left: 130px; width: 340px; height: 200px; z-index: 3; } .cheese { - top: 250px; - left: 80px; + top: 220px; + left: 100px; width: 280px; height: 350px; z-index: 1; } .packaging { - top: 250px; - left: 450px; + top: 220px; + left: 450px; width: 250px; height: 300px; z-index: 4; @@ -183,7 +173,6 @@ $shadow-color: rgba(0, 0, 0, 0.2); } } - .content.hero-page { flex: none; width: 100%; diff --git a/frontend/src/components/NewParticipants/StartupCard.tsx b/frontend/src/components/NewParticipants/StartupCard.tsx new file mode 100644 index 0000000..0282e36 --- /dev/null +++ b/frontend/src/components/NewParticipants/StartupCard.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import type { Startup } from "./types"; +import "./StartupsGrid.scss"; + +interface StartupCardProps { + startup: Partial; +} + +const StartupCard: React.FC = ({ startup }) => { + const name = startup.name || "Без назви"; + const shortDescription = startup.shortDescription || ""; + const category = startup.category || ""; + const location = startup.location || "Невідомо"; + const imageUrl = startup.imageUrl || "/placeholder.png"; + const logoUrl = startup.logoUrl || "/logo-placeholder.png"; + + return ( +
+
+ {name} + {`${name} +
+
+

{category}

+

{name}

+

{shortDescription}

+

{location}

+
+
+ ); +}; + +export default StartupCard; diff --git a/frontend/src/components/NewParticipants/StartupsGrid.scss b/frontend/src/components/NewParticipants/StartupsGrid.scss new file mode 100644 index 0000000..311734c --- /dev/null +++ b/frontend/src/components/NewParticipants/StartupsGrid.scss @@ -0,0 +1,82 @@ +.startups-section { + width: 100%; + background-color: #ffffff; + padding: 0; +} + +.startups-container { + width: 100%; + max-width: 1512px; + margin: 0 auto; + height: 597px; + box-sizing: border-box; + padding: 40px 24px; + gap: 24px; + + @media (min-width: 1800px) { + max-width: 1700px; + } + + @media (min-width: 2100px) { + max-width: 2000px; + } +} + +.startups-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 24px; + width: 100%; +} + +.startups-container .startups-header .title { + font-family: "Geologica"; + font-weight: 700; + font-size: 32px; + line-height: 120%; + letter-spacing: 1%; + color: #232424; + margin: 0; +} + +.startups-container .startups-header .view-all { + font-family: "Geologica"; + font-weight: 700; + font-size: 16px; + color: #232424; + background: none; + border: none; + cursor: pointer; + text-decoration: none; + padding: 0; +} + +.grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 24px; + align-items: start; +} + +.empty-state, +.loading-state { + text-align: center; + color: #555; + font-family: "Geologica"; + font-size: 16px; + margin: 40px 0; +} + +.load-more { + display: block; + margin: 24px auto 0; + font-family: "Geologica"; + font-weight: 700; + font-size: 16px; + padding: 12px 28px; + border: 1px solid #232424; + background: white; + border-radius: 6px; + cursor: pointer; +} diff --git a/frontend/src/components/NewParticipants/StartupsGrid.tsx b/frontend/src/components/NewParticipants/StartupsGrid.tsx new file mode 100644 index 0000000..613ecb8 --- /dev/null +++ b/frontend/src/components/NewParticipants/StartupsGrid.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useState } from "react"; +import StartupCard from "./StartupCard"; +import type { Startup } from "./types"; +import "./StartupsGrid.scss"; + +const PAGE_SIZE = 8; + +const StartupsGrid: React.FC = () => { + const [startups, setStartups] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [loading, setLoading] = useState(false); + + const fetchStartups = async (pageNumber: number) => { + try { + setLoading(true); + + const res = await fetch( + `/api/startups/?page=${pageNumber}&page_size=${PAGE_SIZE}` + ); + const data = await res.json(); + + const mapped = data.results.map((s: any) => ({ + id: s.id, + name: s.company_name, + shortDescription: s.short_description || "", + category: s.tags?.[0] || "Інші послуги", + logoUrl: s.logo_url || "/logo-placeholder.png", + imageUrl: s.banner_url || "/placeholder.png", + location: s.city || "Україна", + })); + + setStartups((prev) => [...prev, ...mapped]); + + setHasMore(Boolean(data.next)); + } catch (err) { + console.error("Error fetching startups:", err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchStartups(1); + }, []); + + const loadMore = () => { + const nextPage = page + 1; + setPage(nextPage); + fetchStartups(nextPage); + }; + + return ( +
+
+
+

Нові учасники

+ +
+ + {loading && startups.length === 0 && ( +
Завантаження...
+ )} + + {!loading && startups.length === 0 && ( +
Наразі немає учасників.
+ )} + +
+ {startups.map((startup) => ( + + ))} +
+ + {hasMore && ( + + )} +
+
+ ); +}; + +export default StartupsGrid; diff --git a/frontend/src/components/NewParticipants/__tests__/StartupsGrid.test.tsx b/frontend/src/components/NewParticipants/__tests__/StartupsGrid.test.tsx new file mode 100644 index 0000000..6cd8e45 --- /dev/null +++ b/frontend/src/components/NewParticipants/__tests__/StartupsGrid.test.tsx @@ -0,0 +1,21 @@ +import { render, screen } from "@testing-library/react"; +import StartupCard from "../StartupCard"; + +const mockStartup = { + id: 1, + name: "Тестова компанія", + category: "Інші послуги", + shortDescription: "Короткий опис", + location: "Україна", + imageUrl: "/placeholder.png", + logoUrl: "/logo-placeholder.png", +}; + +describe("StartupCard basic render", () => { + test("рендерить name, category і location", () => { + render(); + expect(screen.getByText("Тестова компанія")).toBeInTheDocument(); + expect(screen.getByText("Інші послуги")).toBeInTheDocument(); + expect(screen.getByText("Україна")).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/NewParticipants/types.ts b/frontend/src/components/NewParticipants/types.ts new file mode 100644 index 0000000..96b70b0 --- /dev/null +++ b/frontend/src/components/NewParticipants/types.ts @@ -0,0 +1,9 @@ +export interface Startup { + id: number; + name: string; + shortDescription: string; + category: string; + logoUrl: string; + imageUrl: string; + location: string; +} diff --git a/frontend/src/components/WhyWorthGrid/WhyWorthGrid.scss b/frontend/src/components/WhyWorthGrid/WhyWorthGrid.scss index a764a42..0ff93e6 100644 --- a/frontend/src/components/WhyWorthGrid/WhyWorthGrid.scss +++ b/frontend/src/components/WhyWorthGrid/WhyWorthGrid.scss @@ -9,6 +9,14 @@ margin: 0 auto; padding: 0 24px; + @media (min-width: 1800px) { + max-width: 1700px; + } + + @media (min-width: 2100px) { + max-width: 2000px; + } + .section-title { font-family: 'Geologica'; font-weight: 700; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 5c9e879..6695c1b 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,5 +1,6 @@ import React from 'react'; import Hero from '../components/Hero/Hero'; +import StartupsGrid from '../components/NewParticipants/StartupsGrid'; import CTASection from '../components/CTASection/CTASection'; import ForWhomGrid from '../components/ForWhomGrid/ForWhomGrid'; @@ -8,6 +9,7 @@ const Home: React.FC = () => { return ( <> + diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index c831627..29ed429 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -1,5 +1,7 @@ import '@testing-library/jest-dom'; import { TextEncoder, TextDecoder } from 'util'; +import 'whatwg-fetch'; + // @ts-ignore global.TextEncoder = TextEncoder; diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 816c90c..44c527f 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -1,32 +1,34 @@ { - "compilerOptions": { +"compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2022", "useDefineForClassFields": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], "module": "ESNext", - "types": ["vite/client", "jest", "node", "@testing-library/jest-dom"], + "types": ["vite/client", "jest", "node", "@testing-library/jest-dom", "react", "react-dom"], "skipLibCheck": true, - "moduleResolution": "bundler", "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx", - "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, - "noUncheckedSideEffectImports": true + "noUncheckedSideEffectImports": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true }, - "include": ["src"], - - "exclude": [ + + "include": [ + "src", "src/**/*.test.ts", "src/**/*.test.tsx", +], + + "exclude": [ "jest.config.cjs" ] } From 08f31e9cedf8960a78bdbe76223fdd8edd7db5ff Mon Sep 17 00:00:00 2001 From: Someone Account <158297903+Yenot52@users.noreply.github.com> Date: Thu, 20 Nov 2025 10:52:38 +0200 Subject: [PATCH 2/2] Resolve merge conflicts in Jest and TS config --- frontend/jest.config.cjs | 4 +++- frontend/src/setupTests.ts | 33 +++++++++++++++++++++++------ frontend/tsconfig.app.json | 43 ++++++++++++++++++++++++++------------ 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/frontend/jest.config.cjs b/frontend/jest.config.cjs index 3d2f855..7d00934 100644 --- a/frontend/jest.config.cjs +++ b/frontend/jest.config.cjs @@ -1,3 +1,4 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { preset: 'ts-jest', testEnvironment: 'jest-environment-jsdom', @@ -5,8 +6,9 @@ module.exports = { transform: { '^.+\\.tsx?$': ['ts-jest', { tsconfig: '/tsconfig.app.json' }] }, + moduleNameMapper: { - '\\.(css|scss)$': 'identity-obj-proxy', + '\\.(css|scss|sass|less)$': 'identity-obj-proxy', '\\.(jpg|jpeg|png|gif|svg)$': '/src/mocks/fileMock.js' } }; diff --git a/frontend/src/setupTests.ts b/frontend/src/setupTests.ts index 29ed429..926097d 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -2,11 +2,32 @@ import '@testing-library/jest-dom'; import { TextEncoder, TextDecoder } from 'util'; import 'whatwg-fetch'; +if (typeof (global as any).TextDecoder === 'undefined') { + (global as any).TextDecoder = class TextDecoder { + decode(input: any) { + return input.toString(); + } + }; +} -// @ts-ignore -global.TextEncoder = TextEncoder; -// @ts-ignore -global.TextDecoder = TextDecoder; +if (typeof window !== 'undefined') { + if (!window.location.assign) { + window.location.assign = jest.fn(); + } + if (!window.location.replace) { + window.location.replace = jest.fn(); + } + if (!window.location.reload) { + window.location.reload = jest.fn(); + } +} - -window.URL.createObjectURL = jest.fn(() => "mock-url-for-test"); +if (typeof globalThis.fetch === 'undefined') { + globalThis.fetch = jest.fn(() => + Promise.resolve({ + ok: true, + status: 200, + json: async () => ({}), + }) + ) as any; +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index 44c527f..3f1a488 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -1,34 +1,51 @@ { -"compilerOptions": { + "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", - "useDefineForClassFields": true, "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", - "types": ["vite/client", "jest", "node", "@testing-library/jest-dom", "react", "react-dom"], - "skipLibCheck": true, "moduleResolution": "bundler", - "allowImportingTsExtensions": true, "moduleDetection": "force", - "noEmit": true, + "allowImportingTsExtensions": true, + "jsx": "react-jsx", "strict": true, + "skipLibCheck": true, + "noEmit": true, + "noUnusedLocals": true, "noUnusedParameters": true, - "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true, + "erasableSyntaxOnly": true, + "esModuleInterop": true, - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + + "forceConsistentCasingInFileNames": true, + + "types": [ + "vite/client", + "jest", + "node", + "@testing-library/jest-dom", + "react", + "react-dom", + "whatwg-fetch" + ] }, - + "include": [ - "src", + "src", "src/**/*.test.ts", - "src/**/*.test.tsx", -], + "src/**/*.test.tsx" + ], "exclude": [ - "jest.config.cjs" + "jest.config.cjs", + "node_modules", + "dist" ] }