diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..769834e
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+# base url is required n the .env or playwright tests won't work
+BASE_URL="http://localhost:3000"
diff --git a/.github/workflows/housekeeping.yml b/.github/workflows/housekeeping.yml
new file mode 100644
index 0000000..b849acc
--- /dev/null
+++ b/.github/workflows/housekeeping.yml
@@ -0,0 +1,33 @@
+name: lint and test
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+jobs:
+ test:
+ timeout-minutes: 60
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: lts/*
+ - name: Install dependencies
+ run: npm install -g pnpm && pnpm install
+ - name: Install Playwright Browsers
+ run: pnpm exec playwright install --with-deps
+ - name: Lint code
+ run: pnpm lint
+ - name: Run unit tests
+ run: pnpm vitest
+ - name: Start Vite dev server
+ run: pnpm exec vite --port 3000 &
+ - name: Run Playwright tests
+ run: pnpm exec playwright test
+ - uses: actions/upload-artifact@v4
+ if: ${{ !cancelled() }}
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 30
diff --git a/.gitignore b/.gitignore
index b88c813..cc9c178 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,7 +11,7 @@ node_modules
dist
dist-ssr
*.local
-
+.env
# Editor directories and files
.vscode/*
!.vscode/extensions.json
@@ -22,7 +22,10 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+tsconfig.node.tsbuildinfo
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
+tsconfig.app.tsbuildinfo
+codealike.json
diff --git a/Syt.js b/Syt.js
deleted file mode 100644
index 867e22d..0000000
--- a/Syt.js
+++ /dev/null
@@ -1,333 +0,0 @@
-import React from 'react'
-
-export default function Syt() {
- return (
-
- )
-}
diff --git a/docs/BOILERPLATE.md b/docs/BOILERPLATE.md
new file mode 100644
index 0000000..73a604a
--- /dev/null
+++ b/docs/BOILERPLATE.md
@@ -0,0 +1,161 @@
+The project is using
+
+### [tanstack router](https://tanstack.com/query/latest)
+The router which offers a declarative way to navigate between different routes in a React application.
+picked for
+- highly configurable file based routing
+- auto generated typescript types
+- auto code splitting
+- nice serach params handling
+- possible to upgrade to [tanstack start](https://tanstack.com/start/latest) should `SSR` + `SEO` requiremnents arise
+
+
+### [tanstack query (react query)](https://tanstack.com/query/latest)
+Data fetching and caching library
+picked for being the most popular way to handle aysn state in react with caching and preloading capabilities
+
+best practices include
+
+- declaring the `queryOptions` outside of the `useQuery` hook for easy reusability
+
+```tsx
+import { queryOptions } from "@tanstack/react-query";
+interface usersQueryOptionPropss {
+ keyword: string;
+}
+export function usersListQueryOptions({ keyword }: usersQueryOptionPropss) {
+ return queryOptions({
+ queryKey: ["users_list", keyword],
+ queryFn: () => {
+ return new Promise<{
+ items: Array & { id: string }>;
+ }>((res, rej) => {
+ setTimeout(() => {
+ res({
+ items: [{ id: "id_1" }, { id: "id_2" }, { id: "id_3" }],
+ });
+ }, 1000);
+ });
+ },
+ });
+}
+```
+- using the `useSuspenseQuery` over the `useQuery` hook
+ > suspense is the new way tohnadle data fetching in react that enebles [render while you fetch](https://www.epicreact.dev/render-as-you-fetch)
+
+- Invalidate queries on mutation
+ ,
+
+The query client setup includes a way to pass in a meta object to the mutation which will contain an array of keys to invalidate
+```tsx
+// main.tsx
+export const queryClient = new QueryClient({
+ mutationCache: new MutationCache({
+ onSuccess: async (_, __, ___, mutation) => {
+ if (Array.isArray(mutation.meta?.invalidates)) {
+ // biome-ignore lint/complexity/noForEach:
+ mutation.meta?.invalidates.forEach((key) => {
+ return queryClient.invalidateQueries({
+ queryKey: [key.trim()],
+ });
+ });
+ }
+ },
+ }),
+ defaultOptions: {
+ queries: {
+ staleTime: 1000 * 10,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ },
+ },
+});
+// usage
+useMutation({
+ mutationFn: async (value: {}) => {
+ return new Promise<{}>((resolve, reject) => {
+ setTimeout(() => {
+ resolve(value);
+ }, 2000);
+ });
+ },
+ onSuccess: () => {
+ makeHotToast({
+ title: "User added",
+ description: "User has been added successfully",
+ variant: "success",
+ });
+ },
+ meta: {
+ // this will mark any query with the key "users as stale
+ invalidates: ["users"],
+ },
+})
+```
+- add any values that your query depends on into the query key
+```tsx
+function userListQueryOptions({ keyword, first_name,last_name }: usersQueryOptionPropss) {
+ queryClient({
+ queryKey: ["users_list", keyword, first_name, last_name],
+ queryFn: () => {
+ return new Promise<{
+ items: Array & { id: string }>;
+ }>((res, rej) => {
+ setTimeout(() => {
+ res({
+ items: [{ id: "id_1",first_name,last_name }, { id: "id_2".first_name,last_name }, { id: "id_3" }],
+ });
+ }, 1000);
+ });
+ },
+ });
+```
+- the tanstack query eslint plugin will catch most of these common issues
+
+### [tailwind css](https://tailwindcss.com/) + [daisyui](https://daisyui.com/)
+For styling daisyui is a bootstrap like tailwind classes that can help make your taiiwnd cleaner.
+it also comes with nice theme tokens , which are the prefered way of adding background/text colors
+
+```json
+ daisyui: {
+ themes: [
+ {
+ light: {
+ "color-scheme": "light",
+ primary: "#1eb854",
+ "primary-focus": "#188c40",
+ "primary-content": "#241f31",
+ secondary: "#20d55f",
+ "secondary-focus": "#18aa4b",
+ "secondary-content": "#000000",
+ accent: "#d99330",
+ ....
+ },}...
+```
+```tsx
+
+
+ primary button
+
+
Default background
+
Lighter background
+
Lightest background
+<
The default text color
+This will be primary text
+
+```
+
+### [shadcn](https://ui.shadcn.com/)
+
+A set of accessible components built on [radix ui](https://www.radix-ui.com/primitives) with tailwind styles.
+
+### [zod](https://zod.dev/)
+ schema validation that helps in confirming the shape of data returned matches the expectations (local storage , search params...)
+
+
+
+### [react-hook-form](https://www.react-hook-form.com/)
+Form management
+
+### [vitest](https://vitest.dev/) + [playwright](https://playwright.dev/)
+unit testing + E2E
diff --git a/docs/CHECKLIST.MD b/docs/CHECKLIST.MD
new file mode 100644
index 0000000..e39a003
--- /dev/null
+++ b/docs/CHECKLIST.MD
@@ -0,0 +1,14 @@
+# CHECKLIST
+
+Before working on a feature, it's recommended to check out the [issues](https://github.com/SpaceyaTech/CoLabs-Frontend/issues) and assign yourself one. If none of them interest you, you can [check out the Figma file](https://www.figma.com/design/2r8LJbRj7TEXZ7obQvVeb8/SpaceYaTech-Website-Re-design?node-id=4-598&node-type=canvas&t=ZX1LpTU4llEME87H-0) and identify the feature you want to work on.
+- Create an issue on GitHub with the feature you want to work on and assign yourself.
+- Create a branch for the feature you want to work on.
+- Work on your feature.
+- Write some tests to ensure someone else won't break your code.
+- Run the tests, linter, and build to make sure you didn't break anything.
+```sh
+ npm run dryrun
+```
+- If everything is fine, you can make a pull request and await a review.
+- 🎉 🎉 🎉 You did it. Rinse and repeat.
+
diff --git a/docs/TANSTACK-QUERY.md b/docs/TANSTACK-QUERY.md
new file mode 100644
index 0000000..73fc085
--- /dev/null
+++ b/docs/TANSTACK-QUERY.md
@@ -0,0 +1,39 @@
+# tips
+
+- prefer `useSuspenseQuery` over `useQuery`
+- prefer defining `queryOptions` over inlinignthem into the query hooks
+
+ ```tsx
+ import { queryOptions,useSuspenseQuery } from "@tanstack/react-query";
+ //users/queryOptions.ts
+ // this is re-usable and decltters your components
+ export const userQueryOptions = queryOptions({
+ queryKey: ["user"],
+ queryFn: () => {
+ ...
+ }
+ })
+ // users.tsx
+ const query = useSuspenseQuery(userQueryOptions);
+ ```
+- wrap the data fetching components with `Suspense` component
+
+ ```tsx
+ // users page
+
+ Users page
+ Loading users
}>
+
+
+
+ ```
+
+run `p/mpm run page users` to scafold a page with these best practices
+
+
+## handling forms
+
+We'll use [react-hook-form](https://react-hook-form.com/) to manage our forms and [zod](https://zod.dev/) to validate our data.
+[quick tutorial on react hhok form + zod](https://www.youtube.com/watch?v=cc_xmawJ8Kg)
+
+then use the useMutation hook
diff --git a/docs/TANSTACK-ROUTER.md b/docs/TANSTACK-ROUTER.md
new file mode 100644
index 0000000..81b583f
--- /dev/null
+++ b/docs/TANSTACK-ROUTER.md
@@ -0,0 +1,108 @@
+- anything under the src/routes folder is a route
+
+ex
+
+- `src/routes/index.tsx` ->` /`
+- `src/routes/profile.tsx` or `src/routes/profile/index.tsx` -> `/profile/`
+
+Dynamic routes
+
+- `src/routes/users/$oneuser/index.tsx` -> `/users/$oneuser`
+
+inside here you'll have access to the `oneuser` param
+
+```tsx
+// src/routes/users/$user/index.tsx
+import { createFileRoute, useParams } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/users/$user')({
+ component:OneUserComponent,
+})
+functon OneUserComponent() {
+const {user} = useParams({
+ from:"/users/$user"
+})
+ return (
+
+
{user}
+
+ )
+}
+```
+
+if you nest multiple dynamic routes then you can access them in the same way
+
+```tsx
+// src/routes/users/$user/friends/$friend/index.tsx
+import { createFileRoute, useParams } from '@tanstack/react-router'
+
+export const Route = createFileRoute('/users/$user/friends/$friend/')({
+ component: RouteComponent,
+})
+
+function RouteComponent() {
+ const { friend,user } = useParams({
+ from: "/users/$user/friends/$friend/",
+ });
+ return (
+
+
{user}
+
{friend}
+
+ );
+}
+
+
+```
+
+you can also define search params ,validate them and auth guard
+
+```tsx
+import {
+ createFileRoute,
+ redirect,
+ useNavigate,
+ useSearch,
+} from "@tanstack/react-router";
+import { z } from "zod";
+const searchparams = z.object({
+ // make it optional if it won't always be used
+ page: z.number().optional(),
+});
+
+export const Route = createFileRoute("/users/")({
+ component: RouteComponent,
+ // declare and validate your search params here
+ // this page should alwatys have /users?page=1
+ validateSearch: (search) => searchparams.parse(search),
+ // this is how you auth guard routes (only allow logged in users here )
+ async beforeLoad(ctx) {
+ const viewer = ctx.context?.viewer;
+ if (!viewer?.record) {
+ throw redirect({
+ to: "/auth",
+ // this search params will be used to redirect you back to this page once you log in
+ search: { returnTo: ctx.location.pathname },
+ });
+ }
+ },
+});
+
+function RouteComponent() {
+ const { page } = useSearch({
+ from: "/users/",
+ });
+ const navigate = useNavigate({
+ from: "/users",
+ });
+ return (
+
+ {page}
+
+
+ );
+}
+
+```
diff --git a/docs/TESTING.md b/docs/TESTING.md
new file mode 100644
index 0000000..4f75dd8
--- /dev/null
+++ b/docs/TESTING.md
@@ -0,0 +1,76 @@
+# Testing
+
+>[!NOTE]
+> Prefer e2e (playwright) fro testing the ui instead of unit tests. and uase the tooling like recording of tests and locators.
+> use unit tests for units of logic and functions only
+
+
+
+```sh
+npm run test
+```
+will run your unit tests and e2e tests to ensure everything is working correctly.
+
+- [vitest](https://vitest.dev/)
+ Unit testing, ideal for functions (components too).
+
+```sh
+npm run vitest
+```
+
+Ideally, put your unit tests next to your `function.ts(x)` but with `function.test.ts(x)`.
+
+```ts
+import { describe, it, expect } from "vitest";
+import { add } from "./add";
+describe("add", () => {
+ it("should add two numbers", () => {
+ expect(add(1, 2)).toBe(3);
+ });
+});
+```
+
+Component testing can be done with [react testing library](https://testing-library.com/docs/react-testing-library/intro/) or you could only write e2e tests with Playwright instead to avoid double testing and take advantage of the Playwright tooling like recording of tests and locators.
+
+- [playwright](https://playwright.dev/)
+ E2E testing, ideal for UI testing.
+
+```sh
+npm run playwright
+```
+
+All the tests should be in the `e2e-tests` folder.
+
+```tsx
+import { test, expect } from "@playwright/test";
+
+test("has title", async ({ page }) => {
+ await page.goto("https://playwright.dev/");
+ await expect(page).toHaveTitle(/Playwright/);
+});
+```
+
+Add a `data-test` attribute to components you'll query in your tests for more reliable results.
+
+```tsx
+// index.page.tsx
+function HomePage() {
+ return
Colabs
;
+}
+// e2e-tests/homepage/index.spec.ts
+import { test, expect } from "@playwright.test";
+test("has colabs text", async ({ page }) => {
+ await page.goto("/");
+ await expect(
+ page.locator('[data-test="DashboardLayoutHeaderLogo"]'),
+ ).toHaveText("Colabs");
+});
+```
+use features like the locator picker int the playwright vsocede extension to find to use in the tests
+
+## Helpful resources
+
+
+
+- [☝️ elaborarted in this video below](https://youtu.be/ZF1W6FxWOiA?si=AkHqOwLDedPJ3PC6)
+- [Advice on what to test](https://youtu.be/4-_0aTlkqK0?si=my5IJnAmtcIOhpX9)
diff --git a/docs/playwright-vscode-panel.png b/docs/playwright-vscode-panel.png
new file mode 100644
index 0000000..f5bb5d9
Binary files /dev/null and b/docs/playwright-vscode-panel.png differ
diff --git a/e2e-tests/dashboard/dashboard-layout-navbar.spec.ts b/e2e-tests/dashboard/dashboard-layout-navbar.spec.ts
new file mode 100644
index 0000000..72f1a3b
--- /dev/null
+++ b/e2e-tests/dashboard/dashboard-layout-navbar.spec.ts
@@ -0,0 +1,45 @@
+import { test, expect, Locator } from "@playwright/test";
+async function checkDashboardSectionElements(section: Locator) {
+ // section has a search bar
+ await expect(
+ section.locator('[data-test="DashboardLayoutSearchbar"]'),
+ ).toBeVisible();
+ // section has a post project button
+ await expect(
+ section.locator('[data-test="DashboardpostProjectButton"]'),
+ ).toBeVisible();
+ // section has a user dropdown
+ await expect(
+ section.locator('[data-test="DashboardUserDropdown"]'),
+ ).toBeVisible();
+}
+
+test("test dashboard navbar", async ({ page }) => {
+ await page.goto("/dashboard");
+ await expect(page).toHaveTitle(/Dashboard/);
+ const desktopSection = await page.locator(
+ '[data-test="DashboardLayoutHeaderDesktop"]',
+ );
+ const mobileSection = await page.locator(
+ '[data-test="DashboardLayoutHeaderMobile"]',
+ );
+ // desktop section is visible and mobile is not
+ await expect(desktopSection).toBeVisible();
+ await expect(mobileSection).not.toBeVisible();
+ // section has a logo
+ // on mobile the logo section will be hidden and only shown in the sidebar
+ await expect(
+ desktopSection.locator('[data-test="DashboardLayoutHeaderLogo"]'),
+ ).toBeVisible();
+ // check the section contains the expected elements
+ await checkDashboardSectionElements(desktopSection);
+ // mobile section is not visible
+
+ // resize the viewport to mobile
+ await page.setViewportSize({ width: 400, height: 1000 });
+ // monile section is visible and desktop is not
+ await expect(mobileSection).toBeVisible();
+ await expect(desktopSection).not.toBeVisible();
+ // check the section contains the expected elements
+ await checkDashboardSectionElements(mobileSection);
+});
diff --git a/e2e-tests/dashboard/dashboard-layout-sidebar.spec.ts b/e2e-tests/dashboard/dashboard-layout-sidebar.spec.ts
new file mode 100644
index 0000000..29c4d78
--- /dev/null
+++ b/e2e-tests/dashboard/dashboard-layout-sidebar.spec.ts
@@ -0,0 +1,78 @@
+import { test, expect, Locator, Page } from "@playwright/test";
+import { dashboard_routes } from "../../src/components/navigation/routes";
+
+async function checkLinkNameAndNavigateToIt(page: Page, locator: Locator,mobile=false) {
+ for await (const [index, link] of dashboard_routes.entries()) {
+ await expect(await locator.nth(index).textContent()).toBe(link.name);
+ await locator.nth(index).click();
+ await expect(page).toHaveTitle(`Dashboard - ${link.name}`);
+ const breadCrumbs = await page.locator('[data-test="OneTSRBreadCrumb"]');
+ await expect(await breadCrumbs.count()).toBeGreaterThan(0)
+ const breadCrumbName = link.href.split("/").pop();
+ await expect(await breadCrumbs.first().textContent()).toBe(breadCrumbName);
+ await page.goBack();
+ if(mobile){
+ // open the mobile sidebar again
+ await page.locator('[data-test="DashboardLayoutSidebarTrigger"]').click();
+ }
+ }
+}
+
+test("test dashboard sidebar", async ({ page }) => {
+ await page.goto("/dashboard");
+ await expect(page).toHaveTitle(/Dashboard/);
+ const sidebarTrigger = await page.locator(
+ '[data-test="DashboardLayoutSidebarTrigger"]',
+ );
+ await expect(sidebarTrigger).toBeVisible();
+ // sidebar nly shows icons by defalut unti expanded to show text
+ const sidebarLinks = await page.locator(
+ '[data-test="DashboardSidebarLinks"]',
+ );
+ // sidebar is visible by default when in dsktop mode
+ await expect(sidebarLinks).toBeVisible();
+ // select the first link in the sidebar
+ const nestedSidebarLinks = await sidebarLinks.locator(
+ '[data-test="DashboardSidebarLink"]',
+ );
+ await expect(await nestedSidebarLinks.count()).toBe(dashboard_routes.length);
+ await expect(nestedSidebarLinks.first()).toBeVisible();
+ const sidebarLinkName = await nestedSidebarLinks.locator(
+ '[data-test="DashboardSidebarLinkName"]',
+ );
+ // sidebar link name is not visible by default until sidebar is expanded
+ await expect(sidebarLinkName.first()).not.toBeVisible();
+ // click the sidebar trigger to expand the sidebar
+ await sidebarTrigger.click();
+ // sidebar link name is now visible , and clicking should take us to the respective page
+ await checkLinkNameAndNavigateToIt(page, sidebarLinkName);
+ // click tp minimize the sidebar
+ await sidebarTrigger.click();
+ await expect(sidebarLinkName.first()).not.toBeVisible();
+ // resize to mobile view
+ await page.setViewportSize({ width: 400, height: 1000 });
+ // sidebar is not visible by default when in mobile view
+ await expect(sidebarLinks).not.toBeVisible();
+});
+
+test("test mobile dashboard sidebar", async ({ page }) => {
+ await page.setViewportSize({ width: 400, height: 1000 });
+ await page.goto("/dashboard");
+ await expect(page).toHaveTitle(/Dashboard/);
+ const sidebarTrigger = await page.locator(
+ '[data-test="DashboardLayoutSidebarTrigger"]',
+ );
+ await sidebarTrigger.click();
+ // mobile version of the sidebar is visible
+ const mobileSidebar = await page.locator(
+ '[data-sidebar="sidebar"][data-mobile="true"]',
+ );
+ await expect(mobileSidebar).toBeVisible();
+ const mobileSidebarLinks = await mobileSidebar.locator(
+ '[data-test="DashboardSidebarLink"]',
+ );
+ await expect(await mobileSidebarLinks.count()).toBe(dashboard_routes.length);
+ await checkLinkNameAndNavigateToIt(page, mobileSidebarLinks,true);
+});
+
+
diff --git a/e2e-tests/example.spec.ts b/e2e-tests/example.spec.ts
new file mode 100644
index 0000000..f6198a7
--- /dev/null
+++ b/e2e-tests/example.spec.ts
@@ -0,0 +1,9 @@
+import { test, expect } from '@playwright/test';
+
+test('has title', async ({ page }) => {
+ await page.goto('/');
+// Expect a title "to contain" a substring.
+ await expect(page).toHaveTitle(/Colabs/);
+});
+
+
diff --git a/e2e-tests/footer/footer.spec.ts b/e2e-tests/footer/footer.spec.ts
new file mode 100644
index 0000000..24d2ced
--- /dev/null
+++ b/e2e-tests/footer/footer.spec.ts
@@ -0,0 +1,72 @@
+import { test, expect } from "@playwright/test";
+
+test.describe("Footer Component", () => {
+ test.beforeEach(async ({ page }) => {
+ // Navigate to the page where the Footer is rendered
+ await page.goto("/");
+ });
+
+ test("should render the Footer component", async ({ page }) => {
+ const footer = await page.locator('[data-test="Footer"]');
+ await expect(footer).toBeVisible();
+ });
+
+ test("should render social media icons with correct links", async ({
+ page,
+ }) => {
+ // Check for Colabs logo link
+ const colabsLogoLink = page.getByRole("link", { name: "Colabs logo" });
+ await expect(colabsLogoLink).toBeVisible();
+
+ // Check for X icon link
+ const xLink = page.getByRole("link", { name: "X Icon" });
+ await expect(xLink).toBeVisible();
+ await expect(xLink).toHaveAttribute("target", "_blank");
+ const xLinkIcon = xLink.getByRole("img");
+ await expect(xLinkIcon).toHaveAttribute("src", /src\/assets\/x-icon.png/);
+
+
+ // Check for LinkedIn icon link
+ const linkedinLink = page.getByRole("link", { name: "Linkedin Icon" });
+ await expect(linkedinLink).toBeVisible();
+ await expect(linkedinLink).toHaveAttribute("target", "_blank");
+ const linkedinLinkIcon = linkedinLink.getByRole("img");
+ await expect(linkedinLinkIcon).toHaveAttribute("src", /linkedin-icon\.png/);
+ });
+
+ test("should render internal links with correct routes", async ({ page }) => {
+ // Check internal navigation links
+ const aboutLink = page.getByRole("link", { name: "About" });
+ const hackathonsLink = page.getByRole("link", { name: "Hackathons" });
+ const leaderboardLink = page.getByRole("link", { name: "Leaderboard" });
+ const privacyLink = page.getByRole("link", { name: "Privacy & Terms" });
+ const cookiesLink = page.getByRole("link", { name: "Cookies" });
+
+
+ // Assert visibility of links
+ await expect(aboutLink).toBeVisible();
+ await expect(hackathonsLink).toBeVisible();
+ await expect(leaderboardLink).toBeVisible();
+ await expect(privacyLink).toBeVisible();
+ await expect(cookiesLink).toBeVisible();
+
+ // Assert href attributes for navigation links
+ await expect(aboutLink).toHaveAttribute("href", "/");
+ await expect(hackathonsLink).toHaveAttribute(
+ "href",
+ "/dashboard/hackathons",
+ );
+ await expect(leaderboardLink).toHaveAttribute(
+ "href",
+ "/dashboard/leaderboards",
+ );
+ await expect(privacyLink).toHaveAttribute("href", "/");
+ await expect(cookiesLink).toHaveAttribute("href", "/");
+ });
+
+ test("should render copyright notice", async ({ page }) => {
+ const copyrightNotice = page.locator('[data-test="FooterCopyright"]')
+
+ await expect(copyrightNotice).toBeVisible();
+ });
+});
diff --git a/e2e-tests/footer/footerCTA.spec.ts b/e2e-tests/footer/footerCTA.spec.ts
new file mode 100644
index 0000000..4171e5d
--- /dev/null
+++ b/e2e-tests/footer/footerCTA.spec.ts
@@ -0,0 +1,85 @@
+import { test, expect } from "@playwright/test";
+
+test.describe("FooterCTA Component", () => {
+ test.beforeEach(async ({ page }) => {
+ // Replace with the route that renders the FooterCTA component
+ await page.goto("http://localhost:3000");
+ });
+
+ test("should render the FooterCTA component", async ({ page }) => {
+ const footerCTA = await page.locator('[data-test="FooterCTA"]');
+ await expect(footerCTA).toBeVisible();
+ });
+
+ test("should display the main text", async ({ page }) => {
+ const mainText = await page.locator(
+ "text=Experience counts. Get it on Colabs.",
+ );
+ await expect(mainText).toBeVisible();
+ });
+
+ test("should display the secondary text", async ({ page }) => {
+ const secondaryText = await page.locator(
+ "text=Colabs is where you cut your teeth on enterprise projects. We have over 100 repositories on all tech tracks, carefully picked for you.",
+ );
+ await expect(secondaryText).toBeVisible();
+ });
+
+ test("should render the Join Colabs for free button", async ({ page }) => {
+ const joinButton = await page.locator("text=Join Colabs for free");
+ await expect(joinButton).toBeVisible();
+ await expect(joinButton).toHaveAttribute(
+ "href",
+ "/auth/signup?returnTo=%2Fdashboard",
+ );
+ await joinButton.click();
+ });
+
+ test("should render the Launch a Hackathon button", async ({ page }) => {
+ const hackathonButton = await page
+ .locator('[data-test="FooterCTA"]')
+ .getByRole("link", {
+ name: "Launch a Hackathon Rocket icon",
+ });
+ await expect(hackathonButton).toBeVisible();
+ await expect(hackathonButton).toHaveAttribute(
+ "href",
+ "/dashboard/hackathons",
+ );
+ });
+
+ test("should display images", async ({ page }) => {
+ const robotImage = await page
+ .locator('[data-test="FooterCTA"]')
+ .getByRole("img", {
+ name: "Standing robot",
+ });
+ const ellipseImage = await page.getByRole("img", {
+ name: "ellipse background",
+ });
+
+ await expect(robotImage).toBeVisible();
+ await expect(ellipseImage).toBeVisible();
+ });
+
+ test("Join Colabs for free button navigates correctly", async ({ page }) => {
+ const joinButton = await page.locator("text=Join Colabs for free");
+ await expect(joinButton).toBeVisible();
+ await joinButton.click();
+ await expect(joinButton).toHaveAttribute(
+ "href",
+ "/auth/signup?returnTo=%2Fdashboard",
+ );
+ });
+
+ test("Launch a Hackathon button navigates correctly", async ({ page }) => {
+ const hackathonButton = await page
+ .locator('[data-test="FooterCTA"]')
+ .getByRole("link", {
+ name: "Launch a Hackathon Rocket icon",
+ });
+ await expect(hackathonButton).toBeVisible();
+ //setting this up to force a click because the rocket icon is in the way of the button
+ await hackathonButton.click({ force: true });
+ });
+});
diff --git a/e2e-tests/herosection.spec.ts b/e2e-tests/herosection.spec.ts
new file mode 100644
index 0000000..6607d26
--- /dev/null
+++ b/e2e-tests/herosection.spec.ts
@@ -0,0 +1,90 @@
+import { test, expect } from "@playwright/test";
+
+//Test for the HeroSection component
+test.describe("HeroSection Component", () => {
+ test.beforeEach(async ({ page }) => {
+ //go to the page containing the HeroSection component
+ await page.goto("http://localhost:3000");
+ });
+
+ test("should render the HeroSection component", async ({ page }) => {
+ const heroSection = await page.locator('[data-test="HeroSection"]');
+ await expect(heroSection).toBeVisible();
+ });
+
+ test("should display the heading ", async ({ page }) => {
+ const heading = await page.getByRole("heading", {
+ name: "Making open-source contribution fun again",
+ });
+ await expect(heading).toBeVisible();
+ });
+
+ test("should display the description correctly", async ({ page }) => {
+ // Locate the description paragraph within the HeroSection
+ const description = await page.locator(
+ '[data-test="HeroSection"] p.font-ff-inconsolata',
+ );
+
+ // Wait for the paragraph to be visible
+ await expect(description).toBeVisible();
+
+ // Verify the description text
+ await expect(description).toHaveText(
+ "Open-source is the fastest way to get the job experience you need. Colabs is the platform that makes open-source contribution fun and competitive.",
+ );
+ });
+
+ test("should display the collaborator images correctly", async ({ page }) => {
+ const collaborators = await page.locator(
+ '[data-test="HeroSection"] img[alt="Collaborator"]',
+ );
+
+ //check the number of collaborators image displayed
+ await expect(collaborators).toHaveCount(4);
+
+ //check that they are all visible
+ for (let i = 0; i < 4; i++) {
+ const collaborator = collaborators.nth(i);
+ await expect(collaborator).toBeVisible();
+ await expect(collaborator).toHaveClass(/rounded-full/);
+ }
+ });
+
+ test("should display the contributor text", async ({ page }) => {
+ const contributorText = await page.locator(
+ '[data-test="HeroSection"] p.font-text-brand-gray-10',
+ );
+ await expect(contributorText).toHaveText("500+ contributors and counting");
+ });
+
+ test("should render the action buttons correctly", async ({ page }) => {
+ // Verify the "Join Colabs" button
+ const joinButton = await page.locator(
+ "data-test=HeroSection >> text=Join Colabs",
+ );
+ await expect(joinButton).toBeVisible();
+ await expect(joinButton).toHaveAttribute(
+ "href",
+ "/auth/signup?returnTo=%2Fdashboard",
+ );
+
+ // Verify the "Launch a Hackathon" button
+ const hackathonButton = await page.locator(
+ "data-test=HeroSection >> text=Launch a Hackathon",
+ );
+ await expect(hackathonButton).toBeVisible();
+ await expect(hackathonButton).toHaveAttribute(
+ "href",
+ "/dashboard/hackathons",
+ );
+ });
+
+ test("should display the astronaut image", async ({ page }) => {
+ // Verify the astronaut image
+ const astronautImage = await page.locator(
+ '[data-test="HeroSection"] img[alt="Standing robot"]',
+ );
+ await expect(astronautImage).toBeVisible();
+ await expect(astronautImage).toHaveClass(/object-contain/);
+ });
+});
diff --git a/eslint.config.js b/eslint.config.js
index 092408a..98b96f6 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -3,26 +3,39 @@ import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
+import pluginQuery from '@tanstack/eslint-plugin-query'
export default tseslint.config(
- { ignores: ['dist'] },
+ ...pluginQuery.configs["flat/recommended"],
+ { ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
- files: ['**/*.{ts,tsx}'],
+ files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
- 'react-hooks': reactHooks,
- 'react-refresh': reactRefresh,
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
- 'react-refresh/only-export-components': [
- 'warn',
+ "react-refresh/only-export-components": [
+ "warn",
{ allowConstantExport: true },
],
+ 'no-empty-pattern': 'off',
+ 'no-empty-object-type': 'off',
+ 'allowEmptyCatch': 'off',
+ 'allowInterface': 'off',
+ 'no-unused-vars': 'off',
+ "@typescript-eslint/no-empty-object-type": "off",
+ "@typescript-eslint/no-unused-vars": "off",
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-unused-expressions': 'off'
+
},
+
},
-)
+);
diff --git a/index.html b/index.html
index 490d040..d12abc3 100644
--- a/index.html
+++ b/index.html
@@ -1,19 +1,23 @@
-
-
-
-
-
-
-
- Colabs
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+ Colabs
+
+
+
+
+
+
+
+