Skip to content

onlydustxyz/marketplace-frontend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Frontend documentation

Installation

Install Vite - with Homebrew:

brew install vite

Install packages with:

yarn

You can then start the frontend with:

yarn dev

If you plan on running end-to-end tests, use:

yarn dev ---host

Codegen

A code generation plugin is used to automatically generate Typescript types for the frontend, based on the queries provided by Hasura as well as queries defined in queries.graphql files.

It can be launched in watch mode with the following command :

yarn generate --watch

LazyNPM (experimental)

You can use LazyNPM to have a graphical interface allowing to launch the scripts more quickly.

This is convenient but the GUI tends to break down when one of the services encounters an error.

When LazyNPM is installed, you can just launch it at the root of the project with:

lazynpm

Description of the app

The marketplace project is organized in different components and routes. The main entry point for the application is the App component, which is responsible for rendering the different pages and routes of the application. It imports various components and libraries from the React ecosystem, as well as custom components and types from the project.

The App component defines an enumeration called RoutePaths that represents the different routes of the application. It also defines enumerations called ProjectRoutePaths and ProjectPaymentsRoutePaths that represent sub-routes within the project details and project payments pages, respectively.

The App component defines arrays called projectRoutes and routes using the useRoutes hook. These arrays represent the routes of the application and include nested routes and protected routes based on user roles.

The Layout component is responsible for rendering the layout of the marketplace application. It uses various components and hooks to create the desired layout, such as classNames, Outlet, useLocation, ResponsivityFallback, Toaster, Header, and Tooltip. The Layout component conditionally renders the ResponsivityFallback component and controls the visibility of the main content based on the current location.

Development process

Storybook

It is highly encouraged to use Storybook while implementing the design of a component.

Storybook allows to display and implement a component in isolation. It is also possible to display several variants of a component - which makes it the ideal place to describe a design system.

This is easier to do if the component has been properly split between logic and presentation like above.

Interface

Storybook can be launched using the following command:

yarn storybook

This will open an interface (accessible by default locally on the 6006 port).

For each component, a Docs section displays the generated documentation for the component :

Storybook docs

Then, there is a section for each story, with the relevant parameters that can be modified to see the variants of a component.

Storybook story

Creating a story

A story should be named according to the <COMPONENT_NAME>.stories.tsx (e.g. Button.stories.tsx) and should be colocated with the index.tsx and View.tsx files.

Code guidelines

Custom hooks

Complex logic for a use case - particularly with complex useEffect calls - should be extracted in a custom hook.

See the React documentation about when to use custom hooks.

Beware that custom hooks don't share state between components in which they are used - in order to achieve this, you need to use a Context. See this article by Kent C. Dodds on the subject.

Rule of three

Two instances of similar code do not require refactoring, but when similar code is used three times, it should be extracted into a new procedure.

Container and Presentational Components

It is a good idea to separate a component between an index.tsx and a View.tsx file.

See more about this

Making a component

When to add a component to the components folder ?

The folder structure of the app follows what is known as a Fractal structure.

In other words, only add a component to the components folder when it is used in more than two pages or components.

Separating presentation and logic

Components that have complex logic, queries, etc ... should be split in two files inside a dedicated folder: index.tsx for the logical part and View.tsx for the visual part.

Example:

index.tsx

export default function Component() {
  const { data } = useQuery<T>(MY_QUERY);
  return <View items={data} />;
}

View.tsx

interface ComponentViewProps {
  data: T;
}

export default function ComponentView({ data }: ComponentViewProps) {
  return <div className="align-center flex flex-row px-6">// ...</div>;
}

Adding a new page

Page template

export default function Page() {
    return <Background roundedBorders = {BackgroundRoundedBorders.Full} >
    <div className = "flex flex-col justify-center items-center text-greyscale-50" >
        // Rest of content here ...
        </div>
        < /Background>
}

Updating the router

The routes variable in the App component defines the different routes of the application. It is structured as an array and includes routes for the impersonation page, projects page, terms and conditions page, payments page, login page, project details page, catch-all page, and error page.

These routes are defined using the useRoutes hook from the react-router-dom library. The routes are nested and can be accessed by navigating through the application. For example, the project details page has nested routes for the overview, contributors, and payments sub-pages.

Add the route to the routes object in ./frontend/src/App/index.tsx:

const routes = useRoutes([...]);

Example of a page

The Projects component is responsible for rendering a page that displays a list of projects. It imports dependencies and components such as Background, FilterPanel, useAuth, and useT. It defines an enum called Sorting for different sorting options and uses the useLocalStorage hook to store and retrieve the selected sorting option. The component renders the Background component, ProjectFilterProvider, and a list of projects using the AllProjects component.

Example of a component

The ProjectDetails component is responsible for rendering the details of a specific project. It imports dependencies and components such as Navigate, useParams, LanguageMap, ProjectLeadFragment, SponsorFragment, and View. It defines an interface called ProjectDetails for the structure of a project object. The component uses the useParams hook to extract the projectId from the URL parameters and the useProjectVisibility hook to determine if the project is visible to the current user. It conditionally renders the View component or navigates to the Projects route.

Adding a new GraphQL query

Instructions

A new GraphQL query / mutation can be written in the same file as the component that needs it or in a separate queries.graphql / mutations.graphql file, in order for hooks to automatically get generated by the codegen.

See here for more information about how to write GraphQL queries and mutations.

Writing the query

The gql template literal is imported from the @apollo/client package. It is used to define GraphQL queries in the code. In the GithubRepoDetails component, the gql template literal is used to define the GetGithubRepositoryDetails query.

Here's an example of how the gql template literal is used to define the query:

import { gql } from "@apollo/client";

const GetGithubRepositoryDetails = gql`
  query GetGithubRepositoryDetails($githubRepoId: bigint) {
    githubReposByPk(githubRepoId: $githubRepoId) {
      ...GithubRepo
    }
  }

  fragment GithubRepo on GithubRepo {
    name
    description
    // ... other repository fields
  }
`;

The GetGithubRepositoryDetails query fetches the details of a GitHub repository based on the githubRepoId variable. It includes the ...GithubRepo fragment, which specifies the fields to be fetched for the repository.

The gql template literal allows for writing GraphQL queries in a readable and type-safe manner within JavaScript or TypeScript code. It also enables tools like Apollo Client to parse and validate the queries at build time.

In the above example, the query is written inside the component, but ideally the query should be written in a queries.graphql or mutations.graphql file, colocated with the relevant component, just writing what is in the gql tag with the relevant fragments defined before being used, i.e.:

fragment GithubRepo on GithubRepo {
  name
  description
  // ... other repository fields
}

query GetGithubRepositoryDetails($githubRepoId: bigint) {
  githubReposByPk(githubRepoId: $githubRepoId) {
	...GithubRepo
  }
}

Hook

The useGetGithubRepositoryDetailsQuery hook is used to fetch details of a GitHub repository. It is imported from the src/__generated/graphql file. This hook is generated by Apollo Client based on the query added to a queries.graphql file

To use the hook, the githubRepoId prop is passed as a variable to the query. The query itself is defined using the gql template literal tag. It is a GraphQL query named GetGithubRepositoryDetails that takes a single variable githubRepoId of type bigint. The query fetches the details of a GitHub repository with the given ID and includes the ...GithubRepo fragment.

Here's an example of how the useGetGithubRepositoryDetailsQuery hook is used in the GithubRepoDetails component:

import { useGetGithubRepositoryDetailsQuery } from "src/__generated/graphql";

const GithubRepoDetails: React.FC<{ githubRepoId: number }> = ({ githubRepoId }) => {
  const { data } = useGetGithubRepositoryDetailsQuery({
    variables: { githubRepoId },
  });

  // Render the fetched data
  if (data?.githubReposByPk) {
    return <View {...data.githubReposByPk} />;
  }

  return null;
};

Using query results

The View component is imported from the local directory. It is used to render the fetched repository details in the GithubRepoDetails component. The View component likely contains the UI elements and layout for displaying the repository details.

The View component receives the fetched repository details as props. In the example code above, the fetched repository details are spread as props using the spread operator (...data.githubReposByPk). This allows the View component to access and render the specific details of the GitHub repository.

Here's an example of how the View component might be implemented:

type ViewProps = {
  name: string;
  description: string;
  // ... other repository details
};

const View: React.FC<ViewProps> = ({ name, description }) => {
  return (
    <div>
      <h1>{name}</h1>
      <p>{description}</p>
      {/* Render other repository details */}
    </div>
  );
};

Unit tests

Unit tests are used to test utilities and reusable logical components (which are ofter extracted in custom hooks).

They use Vitest and React Testing Library.

Testing utility functions

This is like your regular tests using Jest.

Testing hooks

Hooks can be "rendered" using React Testing Library in order to be tested.

There is an example in the codebase with the useRoles hook:

import { renderHook } from "@testing-library/react-hooks";

it("should return Public role when no token is provided", () => {
  const { result } = renderHook(() => useRoles());
  expect(result.current.roles).toEqual([HasuraUserRole.Public]);
});

The rerender function can be destructured from the result of the renderHook function in order to manually reload a hook.

There is another example of this with the useRoles hook:

it("should update the loggedIn flag when a token is given", () => {
  const { result, rerender } = renderHook((accessToken?: AccessToken) => useRoles(accessToken));
  expect(result.current.isLoggedIn).toBeFalsy();

  const jwtString = "some-token" as AccessToken;
  rerender(jwtString);

  expect(result.current.isLoggedIn).toBeTruthy();
});

Integration tests

Integration test more complex behaviors, namely those of whole components.

Integration tests, like unit tests, use Vitest and React Testing Library, using more sophisticated features of the latter.

End-to-end tests

End-to-end tests are test that launch the app, open a browser and test a few critical user paths.

These tests are made using Playwright.

Running the tests

In order to run the test, the app should be running locally - with the frontend running with the --host flag, see this section for more info.

You can then run:

make playwright/test

Running tests individually

Before running tests individually, the __populate test should be run:

yarn run playwright test --grep __populate

Then any test can be run by specifying a test name's substring after the --grep flag:

yarn run playwright test --grep TEST_NAME_SUBSTRING

Debug mode

Tests can be run in debug mode, i.e. line-per-line with a debugger, using the following command:

yarn run playwright test --grep TEST_NAME_SUBSTRING --debug --reporter line

This allows to either play the test while following which line the test is one, or stepping through the test line-per-line.

Chromatic reviews

Chromatic is a visual regression cloud tool that uses Storybook to check whether differences in component rendering.

Chromatic basically launches Storybook and takes a screenshot of a component for two commits and compares them.

In our case, Chromatic is integrated in our CI/CD pipeline, and it is required to review detected Storbybook changes in Chromatic in order to merge a pull request.

If changes are detected, then the reviewer will have to review each change and accept it manually - to inspect further, it is possible to open the Storybooks of the two branches being compared:

Chromatic view Storybook

See this paragraph for more info on how to use Storybook.

QA Testing

Testing on develop

Changes merged on the main branch can be reviewed on the develop environment:

For the platform:

https://develop.app.onlydust.xyz/

For Hasura:

http://develop.hasura.onlydust.xyz

Deploying on staging

Staging deployment can be done using the following command:

./scripts/promote.sh --staging

This will run a script with several prompts.

At one point, environment variables that have changed compared to the last deployment will be listed. If they haven't been updated yet on Heroku or Vercel (see here), now is the time to do it. You should also update the variables for the production environment.

Launching the QA session

The QA session can be launched by typing /test and pressing the Enter key in the product channel on Slack.

Slack Testing Session

Production deployment

Production deployment can be done using the following command:

./scripts/promote.sh --production

This will run a script with several prompts.

At one point, environment variables that have changed compared to the last deployment will be listed. If they haven't been updated yet on Heroku or Vercel (see here), do it - but it should ideally have been done during staging deployment.

Choosing a task to work on

Bugs

Bugs should be taken in priority when possible. They can be found in the Backlog column of the Engineering board.

Features

Features can also be found in the Backlog column of the Engineering board, and are sorted in descending priority order.

Feature specification

It can be useful to write a breakdown of the necessary steps to complete a task.

This can either be done in the Engineering ticket itself or by creating subtasks in the Build board.

Creating subtasks can be achieved by clicking the Add sub-issues button on the ticket.

Don't forget to choose the Build board as shown below:

adding a task to the Build board

Tech tasks

Tech tasks can be linked to refactoring, perf issues, etc...

They usually have less priority than other features.

Spike

A spike is a task that is meant to start in to investigation into a complex topic (ex: setting up a new tools, investigating logging costs ...).

It is usually timeboxed to one day and should result in creating other tasks with more concrete steps to take.

Snack

A snack is a short task that should be completed in one or two hours.

How to create a project locally ?

This examples works for a local setup but also work in stagin or development by going to the develop or staging Hasura consoles.

Laucnh the hasura console by running:

yarn hasura:console:start

Access the GraphiQL interface (test tube icon tab) of the local Hasura console.

Add a mutation in the lower left corner, and choose the createProject mutation - with as parameters (for example):

mutation MyMutation {
  createProject(
    longDescription: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore ...",
    name: "Lorem",
	shortDescription: "Lorem Ipsum",
	initialBudget: 10000,
	visibility: PUBLIC
  )
}

Run the mutation with the "Play" symbol.

The project will be created

How to become a project leader ?

Then, save the project ID that is displayed by the interface, e.g.:

{
  "data": {
    "createProject": "92c22e51-5e21-44d5-bf51-d8d265849ce5"
  }
}

Finally, you need to invite yourself as a project leader. To achieve this, you will need the project ID shown above as well as you GitHub user ID, which can be found using the GitHub users API, for example: https://api.github.com/users/oscarwroche .

mutation MyMutation {
  inviteProjectLeader(
    githubUserId: 21149076,
	projectId: "92c22e51-5e21-44d5-bf51-d8d265849ce5"
  )
}

Then, go to the marketplace's home page, logged in as the aforementioned GitHub user. You should see an invite that you can accept to become a project leader, and access the payment sending options.

Linking a GitHub repo

It is important to follow these instructions, otherwise multiple issues will be created on public repositories when sending a payment request

Fork an existing repo with the onlydustxyz oragnization.

Then, call the linkGithubRepo mutation using the project ID shown above, and the ID of this repo, which can be found thanks to the Github Repos API, for example: https://api.github.com/repos/facebook/react .

mutation MyMutation {
  linkGithubRepo(
    githubRepoId: 78853160,
	projectId: "92c22e51-5e21-44d5-bf51-d8d265849ce5"
  )
}

Boilerplate / setup

Expect DOM-related stuff

Some content related to the DOM, and accessed with queries / selectors such as screen.findByText(...) needs to be compatible with the Vitest expect function.

This is possible with the following lines of code that are present in every intregration test:

import matchers from "@testing-library/jest-dom/matchers";

expect.extend(matchers);

Mocking authentication mechanisms

A few things are regularly accessed by components, such as user and auth info. The token refresh mechanism hence needs to be mocked.

The following code is often used in the beginning of files (and could / should probably be refactored):

const HASURA_TOKEN_WITH_VALID_JWT_TEST_VALUE = {
  user: {
    id: TEST_USER_ID,
  },
  accessToken: "VALID_ACCESS_TOKEN",
  accessTokenExpiresIn: 900,
  creationDate: new Date().getTime(),
  refreshToken: "test-refresh-token",
};

vi.mock("axios", () => ({
  default: {
    post: (url: string, tokenSet?: TokenSet) => ({
      data: tokenSet?.refreshToken ? HASURA_TOKEN_WITH_VALID_JWT_TEST_VALUE : HASURA_TOKEN_BASIC_TEST_VALUE,
    }),
  },
}));

vi.mock("jwt-decode", () => ({
  default: (jwt: string) => {
    if (jwt === "VALID_ACCESS_TOKEN") {
      return {
        [CLAIMS_KEY]: { [PROJECTS_LED_KEY]: `{"${TEST_PROJECT_ID}"}`, [GITHUB_USERID_KEY]: TEST_GITHUB_USER_ID },
      };
    } else throw "Error";
  },
}));

Mocking GraphQL stuff

GraphQL queries / mutations are often called in high-level components and can be mocked using the same wrapper:

import App, { RoutePaths } from "src/App";
import { GetProjectsDocument } from "src/__generated/graphql";

const ALL_PROJECTS_RESULT: { data: GetProjectsQueryResult["data"] } = {
  data: {
    projects: [
      {
        __typename: "Projects",
        id: TEST_PROJECT_ID,
        // add other fields
      },
    ],
  },
};

const graphQlMocks = [
  {
    request: {
      query: GetProjectsDocument,
      variables: {
        where: {},
      },
    },
    result: ALL_PROJECTS_RESULT,
  },
];

render(<App />, {
  wrapper: MemoryRouterProviderFactory({
    mocks: graphQlMocks,
  }),
});

Be careful to include the __typename in the result queries or fragments, otherwise queries might not be recognized properly.

GraphQL and Apollo Client

Colocate queries with components

Custom queries - those not provided out of the box by the code generation plugin with Hasura - should be created in a queries.graphql file colocated with the component.

The codegen will then automatically create hooks for these custom queries.