diff --git a/frontend/jest.config.cjs b/frontend/jest.config.cjs index 2a9b753..57b458d 100644 --- a/frontend/jest.config.cjs +++ b/frontend/jest.config.cjs @@ -1,15 +1,16 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ + module.exports = { preset: 'ts-jest', - testEnvironment: 'jsdom', - globals: { - 'ts-jest': { - tsconfig: 'tsconfig.app.json', - }, - }, + testEnvironment: 'jest-environment-jsdom', setupFilesAfterEnv: ['/src/setupTests.ts'], - moduleNameMapper: { - '\\.(css|less|scss|sass)$': 'identity-obj-proxy', + transform: { + '^.+\\.tsx?$': ['ts-jest', { tsconfig: '/tsconfig.app.json' }] + }, -}; + moduleNameMapper: { + '\\.(css|scss|sass|less)$': 'identity-obj-proxy', + '\\.(jpg|jpeg|png|gif|svg)$': '/src/mocks/fileMock.js' + } +}; \ No newline at end of file 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 2abe4e0..0dba05a 100644 --- a/frontend/src/setupTests.ts +++ b/frontend/src/setupTests.ts @@ -1,4 +1,6 @@ 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 { @@ -28,4 +30,4 @@ if (typeof globalThis.fetch === 'undefined') { json: async () => ({}), }) ) as any; -} +} \ No newline at end of file diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json index b90ef5b..418bd1b 100644 --- a/frontend/tsconfig.app.json +++ b/frontend/tsconfig.app.json @@ -1,25 +1,85 @@ { + "compilerOptions": { + + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], - "module": "CommonJS", - "moduleResolution": "node", - "isolatedModules": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, + + "module": "ESNext", + + "moduleResolution": "bundler", + + "moduleDetection": "force", + + "allowImportingTsExtensions": true, + + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + + "noEmit": true, + + "noUnusedLocals": true, + + "noUnusedParameters": true, + + "noFallthroughCasesInSwitch": true, + + "noUncheckedSideEffectImports": true, + + "erasableSyntaxOnly": true, + + "esModuleInterop": true, + + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, - "jsx": "react-jsx", + "types": [ + "vite/client", + "jest", + "node", + "@testing-library/jest-dom", + + "react", + + "react-dom", + "whatwg-fetch" - ], - "noEmit": true + + ] + }, - "include": ["src"], - "exclude": ["node_modules", "dist"] + + "include": [ + + "src", + + "src/**/*.test.ts", + + "src/**/*.test.tsx" + + ], + + "exclude": [ + + "jest.config.cjs", + + "node_modules", + + "dist" + + ] + } + +