From 5aca2c48e36664ca25cbaaaad82a98c437705892 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Wed, 10 Jan 2024 11:35:30 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(app-desk)=20create=20a=20basic=20feat?= =?UTF-8?q?ure=20Teams?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As a prove of concept, to check the full process of our token, we create a basic feature Teams. This feature can create a team and list all teams. We use react-query to manage the cache and the request to the API. --- .github/workflows/people.yml | 2 + src/frontend/app/desk/.env.development | 1 + src/frontend/app/desk/.eslintrc.js | 8 +++- src/frontend/app/desk/next.config.js | 12 ++++- src/frontend/app/desk/package.json | 3 ++ .../desk/src/app/Teams/api/useCreateTeam.tsx | 47 +++++++++++++++++++ .../app/desk/src/app/Teams/api/useTeams.tsx | 47 +++++++++++++++++++ src/frontend/app/desk/src/app/Teams/index.tsx | 46 ++++++++++++++++++ src/frontend/app/desk/src/app/layout.tsx | 35 ++++++++------ src/frontend/app/desk/src/app/page.tsx | 9 ++-- src/frontend/app/desk/src/types/api.ts | 6 +++ src/frontend/app/desk/yarn.lock | 33 ++++++++++++- 12 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 src/frontend/app/desk/src/app/Teams/api/useCreateTeam.tsx create mode 100644 src/frontend/app/desk/src/app/Teams/api/useTeams.tsx create mode 100644 src/frontend/app/desk/src/app/Teams/index.tsx create mode 100644 src/frontend/app/desk/src/types/api.ts diff --git a/.github/workflows/people.yml b/.github/workflows/people.yml index 0a86600..3ea5ebd 100644 --- a/.github/workflows/people.yml +++ b/.github/workflows/people.yml @@ -81,6 +81,8 @@ jobs: build-front-desk: runs-on: ubuntu-latest + env: + NEXT_PUBLIC_API_URL: http://localhost:8071/api/v1.0/ defaults: run: working-directory: src/frontend/app/desk diff --git a/src/frontend/app/desk/.env.development b/src/frontend/app/desk/.env.development index 472706a..e636370 100644 --- a/src/frontend/app/desk/.env.development +++ b/src/frontend/app/desk/.env.development @@ -1,3 +1,4 @@ +NEXT_PUBLIC_API_URL=http://localhost:8071/api/v1.0/ NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080/ NEXT_PUBLIC_KEYCLOAK_REALM=people NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=people-front diff --git a/src/frontend/app/desk/.eslintrc.js b/src/frontend/app/desk/.eslintrc.js index a9207f4..0b947d5 100644 --- a/src/frontend/app/desk/.eslintrc.js +++ b/src/frontend/app/desk/.eslintrc.js @@ -1,5 +1,9 @@ module.exports = { - extends: ['next', 'plugin:prettier/recommended'], + extends: [ + 'next', + 'plugin:prettier/recommended', + 'plugin:@tanstack/eslint-plugin-query/recommended', + ], settings: { next: { rootDir: __dirname, @@ -12,7 +16,6 @@ module.exports = { }, rules: { 'block-scoped-var': 'error', - 'no-alert': 'error', 'import/no-duplicates': ['error', { considerQueryString: false }], 'import/order': [ 'error', @@ -38,6 +41,7 @@ module.exports = { 'newlines-between': 'always', }, ], + 'no-alert': 'error', 'no-unused-vars': [ 'error', { varsIgnorePattern: '^_', argsIgnorePattern: '^_' }, diff --git a/src/frontend/app/desk/next.config.js b/src/frontend/app/desk/next.config.js index 658404a..90829d9 100644 --- a/src/frontend/app/desk/next.config.js +++ b/src/frontend/app/desk/next.config.js @@ -1,4 +1,14 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + trailingSlash: true, + async rewrites() { + return [ + { + source: '/api/:slug*/', + destination: `${process.env.NEXT_PUBLIC_API_URL}:slug*/`, // Matched parameters can be used in the destination + }, + ]; + }, +}; module.exports = nextConfig; diff --git a/src/frontend/app/desk/package.json b/src/frontend/app/desk/package.json index 2aaaaf7..755111f 100644 --- a/src/frontend/app/desk/package.json +++ b/src/frontend/app/desk/package.json @@ -15,12 +15,15 @@ }, "dependencies": { "@openfun/cunningham-react": "2.4.0", + "@tanstack/react-query": "5.17.1", "next": "14.0.4", "react": "18.2.0", "react-dom": "18.2.0", "zustand": "4.4.7" }, "devDependencies": { + "@tanstack/eslint-plugin-query": "5.17.1", + "@tanstack/react-query-devtools": "5.17.12", "@testing-library/jest-dom": "6.2.0", "@testing-library/react": "14.1.2", "@types/jest": "29.5.11", diff --git a/src/frontend/app/desk/src/app/Teams/api/useCreateTeam.tsx b/src/frontend/app/desk/src/app/Teams/api/useCreateTeam.tsx new file mode 100644 index 0000000..25320ea --- /dev/null +++ b/src/frontend/app/desk/src/app/Teams/api/useCreateTeam.tsx @@ -0,0 +1,47 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import useAuthStore from '@/auth/useAuthStore'; + +import { KEY_LIST_TEAM } from './useTeams'; + +type CreateTeamResponse = { + id: string; + name: string; +}; +export interface CreateTeamResponseError { + detail: string; +} + +export const createTeam = async (name: string) => { + const { token } = useAuthStore.getState(); + + const response = await fetch(`/api/teams/`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + method: 'POST', + body: JSON.stringify({ + name, + }), + }); + + if (!response.ok) { + throw new Error(`Couldn't create team: ${response.statusText}`); + } + + return response.json(); +}; + +export function useCreateTeam() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: createTeam, + onSuccess: () => { + void queryClient.invalidateQueries({ + queryKey: [KEY_LIST_TEAM], + exact: true, + }); + }, + }); +} diff --git a/src/frontend/app/desk/src/app/Teams/api/useTeams.tsx b/src/frontend/app/desk/src/app/Teams/api/useTeams.tsx new file mode 100644 index 0000000..6afd200 --- /dev/null +++ b/src/frontend/app/desk/src/app/Teams/api/useTeams.tsx @@ -0,0 +1,47 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query'; + +import useAuthStore from '@/auth/useAuthStore'; +import { APIList } from '@/types/api'; + +interface TeamResponse { + id: string; + name: string; +} + +type TeamsResponse = APIList; +export interface TeamsResponseError { + detail: string; +} + +export const getTeams = async () => { + const token = useAuthStore.getState().token; + + const response = await fetch(`/api/teams/`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error(`Couldn't fetch teams: ${response.statusText}`); + } + + return response.json(); +}; + +export const KEY_LIST_TEAM = 'teams'; + +export function useTeams( + queryConfig?: UseQueryOptions< + TeamsResponse, + TeamsResponseError, + TeamsResponse + >, +) { + return useQuery({ + queryKey: [KEY_LIST_TEAM], + queryFn: getTeams, + ...queryConfig, + }); +} diff --git a/src/frontend/app/desk/src/app/Teams/index.tsx b/src/frontend/app/desk/src/app/Teams/index.tsx new file mode 100644 index 0000000..6b6c91a --- /dev/null +++ b/src/frontend/app/desk/src/app/Teams/index.tsx @@ -0,0 +1,46 @@ +import { Button, Field, Input, Loader } from '@openfun/cunningham-react'; +import React, { useState } from 'react'; + +import { useCreateTeam } from './api/useCreateTeam'; +import { useTeams } from './api/useTeams'; + +export const Teams = () => { + const { data, isPending } = useTeams(); + const { mutate: createTeam } = useCreateTeam(); + const [teamName, setTeamName] = useState(''); + + if (isPending) + return ( +
+ +
+ ); + + return ( + <> + + setTeamName(e.target.value)} + /> + + +
+
    + {data?.results.map((post, index) => ( +
  • +
    + + {index + 1}. {post.name} + +
    +
  • + ))} +
+
+ + ); +}; diff --git a/src/frontend/app/desk/src/app/layout.tsx b/src/frontend/app/desk/src/app/layout.tsx index 0ba8a2b..ab1dd33 100644 --- a/src/frontend/app/desk/src/app/layout.tsx +++ b/src/frontend/app/desk/src/app/layout.tsx @@ -1,12 +1,14 @@ 'use client'; import { CunninghamProvider, Switch } from '@openfun/cunningham-react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { Inter } from 'next/font/google'; import { useState } from 'react'; - import './globals.css'; const inter = Inter({ subsets: ['latin'] }); +const queryClient = new QueryClient(); export default function RootLayout({ children, @@ -18,20 +20,23 @@ export default function RootLayout({ return ( - -
- setThemeDark(!themeDark)} - checked={themeDark} - /> - {children} -
-
+ + + +
+ setThemeDark(!themeDark)} + checked={themeDark} + /> + {children} +
+
+
); diff --git a/src/frontend/app/desk/src/app/page.tsx b/src/frontend/app/desk/src/app/page.tsx index 8905987..8bfb9e9 100644 --- a/src/frontend/app/desk/src/app/page.tsx +++ b/src/frontend/app/desk/src/app/page.tsx @@ -1,12 +1,13 @@ 'use client'; -import { Button, Loader } from '@openfun/cunningham-react'; +import { Loader } from '@openfun/cunningham-react'; import { useEffect } from 'react'; -import styles from './page.module.css'; - import useAuthStore from '@/auth/useAuthStore'; +import { Teams } from './Teams'; +import styles from './page.module.css'; + export default function Home() { const { initAuth, authenticated } = useAuthStore(); @@ -21,7 +22,7 @@ export default function Home() { return (

Hello world!

- +
); } diff --git a/src/frontend/app/desk/src/types/api.ts b/src/frontend/app/desk/src/types/api.ts new file mode 100644 index 0000000..5c48a79 --- /dev/null +++ b/src/frontend/app/desk/src/types/api.ts @@ -0,0 +1,6 @@ +export interface APIList { + count: number; + next?: string | null; + previous?: string | null; + results: T[]; +} diff --git a/src/frontend/app/desk/yarn.lock b/src/frontend/app/desk/yarn.lock index 40e06eb..f3367ea 100644 --- a/src/frontend/app/desk/yarn.lock +++ b/src/frontend/app/desk/yarn.lock @@ -1970,6 +1970,37 @@ dependencies: tslib "^2.4.0" +"@tanstack/eslint-plugin-query@5.17.1": + version "5.17.1" + resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.17.1.tgz#e029ae3a2c35ba915e81c50e210a6138e7dd3aea" + integrity sha512-KWag75cTqs+/t3DSuoyM1Vh9Ns4FOnXZOkCn8nkxvOWnHRBL073c9Kg3YiYP3uo8ftcb1iiw3+a/5w1bf+8A7A== + dependencies: + "@typescript-eslint/utils" "^5.62.0" + +"@tanstack/query-core@5.17.1": + version "5.17.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.17.1.tgz#341d975dce20c5c9e0767a3a69c0bfbc9ca16114" + integrity sha512-kUXozQmU7NBtzX5dM6qfFNZN+YK/9Ct37hnG/ogdgI4mExIx7VH/qRepsPhKfNrJz2w81/JykmM3Uug6sVpUSw== + +"@tanstack/query-devtools@5.17.7": + version "5.17.7" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.17.7.tgz#9f9e44a32d08ecd5c9fe3ede62a114d6d6e240d5" + integrity sha512-TfgvOqza5K7Sk6slxqkRIvXlEJoUoPSsGGwpuYSrpqgSwLSSvPPpZhq7hv7hcY5IvRoTNGoq6+MT01C/jILqoQ== + +"@tanstack/react-query-devtools@5.17.12": + version "5.17.12" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.17.12.tgz#61b62a230472243931982d0c802d1a1bc164fc5d" + integrity sha512-5n2oqe9GUD7+QNJqfFm8RqQVVFOeaLQDujBnhyyILDq8XPB6wymTEaS6OJ/CdrhCYKoQe3Uh8E6u9ChABEMuhA== + dependencies: + "@tanstack/query-devtools" "5.17.7" + +"@tanstack/react-query@5.17.1": + version "5.17.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.17.1.tgz#37efbb4573d3f4cc8c62e5ea6c7398d27f52db2a" + integrity sha512-4JYgX0kU+pvwVQi5eRiHGvBK7WnahEl6lmaxd32ZVSKmByAxLgaewoxBR03cdDNse8lUD2zGOe0sx3M/EGRlmA== + dependencies: + "@tanstack/query-core" "5.17.1" + "@tanstack/react-table@8.10.7": version "8.10.7" resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.10.7.tgz#733f4bee8cf5aa19582f944dd0fd3224b21e8c94" @@ -2299,7 +2330,7 @@ "@typescript-eslint/typescript-estree" "6.18.1" semver "^7.5.4" -"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0": +"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0", "@typescript-eslint/utils@^5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==