diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..080ebd8 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "nfs-atlas-3" + } +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 57b0dc5..2c903f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,12 +7,14 @@ on: - epic/* jobs: - build: + test: runs-on: ubuntu-latest if: github.event.pull_request.draft == false + environment: test + env: - VITE_IGNORE_MSW: false + VITE_FIREBASE_KEY: ${{ secrets.VITE_FIREBASE_KEY }} steps: - name: Checkout code @@ -24,8 +26,19 @@ jobs: node-version: 20 cache: "yarn" + - name: Cache Firebase Emulator + uses: actions/cache@v4 + with: + path: ~/.cache/firebase/emulators + key: ${{ runner.os }}-firebase-emulator-${{ hashFiles('**/firebase.json') }} + restore-keys: | + ${{ runner.os }}-firebase-emulator- + + - name: Install Firebase Emulator Suite + run: npm install -g firebase-tools + - name: Install dependencies run: yarn - name: Run Vitest - run: yarn vitest + run: firebase emulators:exec "yarn test" diff --git a/database.rules.json b/database.rules.json new file mode 100644 index 0000000..c0aa595 --- /dev/null +++ b/database.rules.json @@ -0,0 +1,6 @@ +{ + "rules": { + ".read": "auth != null", + ".write": "auth != null" + } +} \ No newline at end of file diff --git a/firebase.json b/firebase.json new file mode 100644 index 0000000..ee016b6 --- /dev/null +++ b/firebase.json @@ -0,0 +1,24 @@ +{ + "emulators": { + "auth": { + "port": 9099 + }, + "firestore": { + "port": 8080 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true, + "storage": { + "port": 9199 + } + }, + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "storage": { + "rules": "storage.rules" + } +} diff --git a/firebase/auth_export/accounts.json b/firebase/auth_export/accounts.json new file mode 100644 index 0000000..b33bd5b --- /dev/null +++ b/firebase/auth_export/accounts.json @@ -0,0 +1 @@ +{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"LwOhVROfZ4XoQ0gl63KIQMyjuXNN","createdAt":"1716649824003","lastLoginAt":"1716972075102","displayName":"Test User","photoUrl":"","passwordHash":"fakeHash:salt=fakeSaltjAJOjdJPCzyTaFZ6C7rl:password=password","salt":"fakeSaltjAJOjdJPCzyTaFZ6C7rl","passwordUpdatedAt":1716903404914,"providerUserInfo":[{"providerId":"password","email":"user1@example.com","federatedId":"user1@example.com","rawId":"user1@example.com","displayName":"Test User","photoUrl":""}],"validSince":"1716903404","email":"user1@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2024-05-29T08:41:15.102Z"}]} \ No newline at end of file diff --git a/firebase/auth_export/config.json b/firebase/auth_export/config.json new file mode 100644 index 0000000..6f240f7 --- /dev/null +++ b/firebase/auth_export/config.json @@ -0,0 +1 @@ +{"signIn":{"allowDuplicateEmails":false},"emailPrivacyConfig":{"enableImprovedEmailPrivacy":false}} \ No newline at end of file diff --git a/firebase/firebase-export-metadata.json b/firebase/firebase-export-metadata.json new file mode 100644 index 0000000..5d398db --- /dev/null +++ b/firebase/firebase-export-metadata.json @@ -0,0 +1,16 @@ +{ + "version": "13.10.1", + "firestore": { + "version": "1.19.6", + "path": "firestore_export", + "metadata_file": "firestore_export/firestore_export.overall_export_metadata" + }, + "auth": { + "version": "13.10.1", + "path": "auth_export" + }, + "storage": { + "version": "13.10.1", + "path": "storage_export" + } +} \ No newline at end of file diff --git a/firebase/firestore_export/firestore_export.overall_export_metadata b/firebase/firestore_export/firestore_export.overall_export_metadata new file mode 100644 index 0000000..92b92aa Binary files /dev/null and b/firebase/firestore_export/firestore_export.overall_export_metadata differ diff --git a/firebase/storage_export/buckets.json b/firebase/storage_export/buckets.json new file mode 100644 index 0000000..e80c228 --- /dev/null +++ b/firebase/storage_export/buckets.json @@ -0,0 +1,7 @@ +{ + "buckets": [ + { + "id": "nfs-atlas-3.appspot.com" + } + ] +} \ No newline at end of file diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 0000000..415027e --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,4 @@ +{ + "indexes": [], + "fieldOverrides": [] +} diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..cd1a434 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,9 @@ +rules_version = '2'; + +service cloud.firestore { + match /databases/{database}/documents { + match /{document=**} { + allow read, write: if false; + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 252c8fc..364f0af 100644 --- a/package.json +++ b/package.json @@ -16,10 +16,9 @@ "@types/js-cookie": "^3.0.6", "@uidotdev/usehooks": "^2.4.1", "date-fns": "^3.3.1", - "js-cookie": "^3.0.5", + "firebase": "^10.12.1", "localforage": "^1.10.0", "match-sorter": "^6.3.4", - "msw": "^2.1.5", "react": "^18.2.0", "react-dom": "^18.2.0", "react-hook-form": "^7.51.2", @@ -36,17 +35,20 @@ "@typescript-eslint/eslint-plugin": "^6.20.0", "@typescript-eslint/parser": "^6.20.0", "@vitejs/plugin-react": "^4.2.1", + "@vitest/browser": "^1.6.0", "eslint": "^8.56.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "firebase-tools": "^13.10.0", "jsdom": "^24.0.0", "prettier": "3.2.5", "tsc-alias": "^1.8.8", "typescript": "^5.4.2", "vite": "^5.0.8", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.2.2" + "vitest": "^1.2.2", + "webdriverio": "^8.37.0" }, "msw": { "workerDirectory": [ diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js deleted file mode 100644 index 1af3a7c..0000000 --- a/public/mockServiceWorker.js +++ /dev/null @@ -1,287 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ - -/** - * Mock Service Worker (2.1.5). - * @see https://github.com/mswjs/msw - * - Please do NOT modify this file. - * - Please do NOT serve this file on production. - */ - -const INTEGRITY_CHECKSUM = "223d191a56023cd36aa88c802961b911"; -const IS_MOCKED_RESPONSE = Symbol("isMockedResponse"); -const activeClientIds = new Set(); - -self.addEventListener("install", function () { - self.skipWaiting(); -}); - -self.addEventListener("activate", function (event) { - event.waitUntil(self.clients.claim()); -}); - -self.addEventListener("message", async function (event) { - const clientId = event.source.id; - - if (!clientId || !self.clients) { - return; - } - - const client = await self.clients.get(clientId); - - if (!client) { - return; - } - - const allClients = await self.clients.matchAll({ - type: "window", - }); - - switch (event.data) { - case "KEEPALIVE_REQUEST": { - sendToClient(client, { - type: "KEEPALIVE_RESPONSE", - }); - break; - } - - case "INTEGRITY_CHECK_REQUEST": { - sendToClient(client, { - type: "INTEGRITY_CHECK_RESPONSE", - payload: INTEGRITY_CHECKSUM, - }); - break; - } - - case "MOCK_ACTIVATE": { - activeClientIds.add(clientId); - - sendToClient(client, { - type: "MOCKING_ENABLED", - payload: true, - }); - break; - } - - case "MOCK_DEACTIVATE": { - activeClientIds.delete(clientId); - break; - } - - case "CLIENT_CLOSED": { - activeClientIds.delete(clientId); - - const remainingClients = allClients.filter((client) => { - return client.id !== clientId; - }); - - // Unregister itself when there are no more clients - if (remainingClients.length === 0) { - self.registration.unregister(); - } - - break; - } - } -}); - -self.addEventListener("fetch", function (event) { - const { request } = event; - - // Bypass navigation requests. - if (request.mode === "navigate") { - return; - } - - // Opening the DevTools triggers the "only-if-cached" request - // that cannot be handled by the worker. Bypass such requests. - if (request.cache === "only-if-cached" && request.mode !== "same-origin") { - return; - } - - // Bypass all requests when there are no active clients. - // Prevents the self-unregistered worked from handling requests - // after it's been deleted (still remains active until the next reload). - if (activeClientIds.size === 0) { - return; - } - - // Generate unique request ID. - const requestId = crypto.randomUUID(); - event.respondWith(handleRequest(event, requestId)); -}); - -async function handleRequest(event, requestId) { - const client = await resolveMainClient(event); - const response = await getResponse(event, client, requestId); - - // Send back the response clone for the "response:*" life-cycle events. - // Ensure MSW is active and ready to handle the message, otherwise - // this message will pend indefinitely. - if (client && activeClientIds.has(client.id)) { - (async function () { - const responseClone = response.clone(); - - sendToClient( - client, - { - type: "RESPONSE", - payload: { - requestId, - isMockedResponse: IS_MOCKED_RESPONSE in response, - type: responseClone.type, - status: responseClone.status, - statusText: responseClone.statusText, - body: responseClone.body, - headers: Object.fromEntries(responseClone.headers.entries()), - }, - }, - [responseClone.body], - ); - })(); - } - - return response; -} - -// Resolve the main client for the given event. -// Client that issues a request doesn't necessarily equal the client -// that registered the worker. It's with the latter the worker should -// communicate with during the response resolving phase. -async function resolveMainClient(event) { - const client = await self.clients.get(event.clientId); - - if (client?.frameType === "top-level") { - return client; - } - - const allClients = await self.clients.matchAll({ - type: "window", - }); - - return allClients - .filter((client) => { - // Get only those clients that are currently visible. - return client.visibilityState === "visible"; - }) - .find((client) => { - // Find the client ID that's recorded in the - // set of clients that have registered the worker. - return activeClientIds.has(client.id); - }); -} - -async function getResponse(event, client, requestId) { - const { request } = event; - - // Clone the request because it might've been already used - // (i.e. its body has been read and sent to the client). - const requestClone = request.clone(); - - function passthrough() { - const headers = Object.fromEntries(requestClone.headers.entries()); - - // Remove internal MSW request header so the passthrough request - // complies with any potential CORS preflight checks on the server. - // Some servers forbid unknown request headers. - delete headers["x-msw-intention"]; - - return fetch(requestClone, { headers }); - } - - // Bypass mocking when the client is not active. - if (!client) { - return passthrough(); - } - - // Bypass initial page load requests (i.e. static assets). - // The absence of the immediate/parent client in the map of the active clients - // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet - // and is not ready to handle requests. - if (!activeClientIds.has(client.id)) { - return passthrough(); - } - - // Bypass requests with the explicit bypass header. - // Such requests can be issued by "ctx.fetch()". - const mswIntention = request.headers.get("x-msw-intention"); - if (["bypass", "passthrough"].includes(mswIntention)) { - return passthrough(); - } - - // Notify the client that a request has been intercepted. - const requestBuffer = await request.arrayBuffer(); - const clientMessage = await sendToClient( - client, - { - type: "REQUEST", - payload: { - id: requestId, - url: request.url, - mode: request.mode, - method: request.method, - headers: Object.fromEntries(request.headers.entries()), - cache: request.cache, - credentials: request.credentials, - destination: request.destination, - integrity: request.integrity, - redirect: request.redirect, - referrer: request.referrer, - referrerPolicy: request.referrerPolicy, - body: requestBuffer, - keepalive: request.keepalive, - }, - }, - [requestBuffer], - ); - - switch (clientMessage.type) { - case "MOCK_RESPONSE": { - return respondWithMock(clientMessage.data); - } - - case "MOCK_NOT_FOUND": { - return passthrough(); - } - } - - return passthrough(); -} - -function sendToClient(client, message, transferrables = []) { - return new Promise((resolve, reject) => { - const channel = new MessageChannel(); - - channel.port1.onmessage = (event) => { - if (event.data && event.data.error) { - return reject(event.data.error); - } - - resolve(event.data); - }; - - client.postMessage( - message, - [channel.port2].concat(transferrables.filter(Boolean)), - ); - }); -} - -async function respondWithMock(response) { - // Setting response status code to 0 is a no-op. - // However, when responding with a "Response.error()", the produced Response - // instance will have status code set to 0. Since it's not possible to create - // a Response instance with status code 0, handle that use-case separately. - if (response.status === 0) { - return Response.error(); - } - - const mockedResponse = new Response(response.body, response); - - Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { - value: true, - enumerable: true, - }); - - return mockedResponse; -} diff --git a/src/assets/css/authentication/LoginFormComponent.module.css b/src/assets/css/authentication/LoginFormComponent.module.css index c04ccfd..5f90c45 100644 --- a/src/assets/css/authentication/LoginFormComponent.module.css +++ b/src/assets/css/authentication/LoginFormComponent.module.css @@ -1,5 +1,3 @@ -@import url("https://fonts.cdnfonts.com/css/dm-sans"); - .login { width: 70%; background-color: white; @@ -7,7 +5,6 @@ display: flex; flex-direction: column; align-items: center; - font-family: "DM Sans", sans-serif; } .login-image { @@ -39,13 +36,11 @@ border-radius: 7px; padding: 12px; background-color: #d9d9d9; - font-family: "DM Sans"; font-size: 20px; } .login-form > input::placeholder { color: #fff; - font-family: "DM Sans"; font-size: 20px; font-style: normal; font-weight: 500; @@ -55,7 +50,6 @@ .forgot-password { color: #151515; - font-family: "DM Sans"; font-size: 15px; text-decoration: underline; } @@ -83,14 +77,13 @@ color: #fff; background-color: #0c1747 !important; - font-family: "DM Sans"; font-size: 20px; text-align: center; cursor: pointer; } /* For desktops */ -@media (min-width: 992px) { +@media screen and (min-width: 992px) { .login { max-width: 1037px; background-color: white; @@ -102,7 +95,6 @@ } .login-image { - width: 50%; - height: 100%; + width: auto; } } diff --git a/src/assets/css/reset.css b/src/assets/css/reset.css index ea7f81d..01556a2 100644 --- a/src/assets/css/reset.css +++ b/src/assets/css/reset.css @@ -3,6 +3,9 @@ GitHub page: https://github.com/elad2412/the-new-css-reset ***/ +@import url("https://fonts.cdnfonts.com/css/dm-sans"); + + /* Remove all the styles of the "User-Agent-Stylesheet", except for the 'display' property - The "symbol *" part is to solve Firefox SVG sprite bug diff --git a/src/components/authentication/LoginFormComponent.tsx b/src/components/authentication/LoginFormComponent.tsx index 2cf0859..f9f1c3b 100644 --- a/src/components/authentication/LoginFormComponent.tsx +++ b/src/components/authentication/LoginFormComponent.tsx @@ -1,10 +1,12 @@ import { useState } from "react"; import { useForm, SubmitHandler } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; import { Icon } from "@iconify/react"; import styles from "css/authentication/LoginFormComponent.module.css"; import loginImage from "/login-image.svg"; -import { signIn } from "@/api/authentication.ts"; +import { auth } from "@/firebase"; +import { signInWithEmailAndPassword } from "firebase/auth"; type Inputs = { email: string, @@ -13,12 +15,24 @@ type Inputs = { const LoginFormComponent = () => { const [showPassword, setShowPassword] = useState(false); + const navigate = useNavigate(); const { register, handleSubmit, } = useForm(); - const onSubmit: SubmitHandler = (data) => signIn(data.email, data.password); + const onSubmit: SubmitHandler = (data) => { + // signIn(data.email, data.password); + signInWithEmailAndPassword(auth, data.email, data.password) + .then(() => { + navigate("/"); + }) + .catch((error) => { + const errorCode = error.code; + const errorMessage = error.message; + console.log(errorCode, errorMessage); + }); + } const changePasswordVisibility = () => { setShowPassword((prev) => !prev); diff --git a/src/firebase.ts b/src/firebase.ts new file mode 100644 index 0000000..f12175e --- /dev/null +++ b/src/firebase.ts @@ -0,0 +1,19 @@ +import { initializeApp } from "firebase/app"; +import { getAuth, connectAuthEmulator } from "firebase/auth"; + +const firebaseConfig = { + apiKey: import.meta.env.VITE_FIREBASE_KEY, + authDomain: "nfs-atlas-3.firebaseapp.com", + projectId: "nfs-atlas-3", + storageBucket: "nfs-atlas-3.appspot.com", + messagingSenderId: "652074107095", + appId: "1:652074107095:web:c8ed60b1afb4dc44217092" +}; + +const app = initializeApp(firebaseConfig); + +export const auth = getAuth(app); + +if (import.meta.env.DEV) { + connectAuthEmulator(auth, "http://localhost:9099"); +} diff --git a/src/main.tsx b/src/main.tsx index aa82700..8c9b980 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -9,26 +9,11 @@ import "css/reset.css"; const queryClient = new QueryClient(); -async function enableMocking() { - if ( - !import.meta.env.DEV || - import.meta.env.VITE_IGNORE_MSW.toLowerCase() === "true" - ) { - return; - } - - const { worker } = await import("@/mocks/worker"); - - return worker.start({ onUnhandledRequest: "bypass" }); -} - -enableMocking().then(() => { - ReactDOM.createRoot(document.getElementById("root")!).render( - - - - - - , - ); -}); +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + , +); diff --git a/src/mocks/authentication/auth_handlers.ts b/src/mocks/authentication/auth_handlers.ts deleted file mode 100644 index 7b25d6e..0000000 --- a/src/mocks/authentication/auth_handlers.ts +++ /dev/null @@ -1,53 +0,0 @@ -//src/mocks/authentication/auth_handlers.ts - -import { http, HttpResponse } from "msw"; -import resolveURL from "../../api/fetch"; - -export const mockCSRFToken: string = "SUPERSECRETCSRFTOKEN"; -export const mockUsername: string = "testuser"; -export const mockPassword: string = "mockpassword"; - -type responseData = { - username: string | undefined; - password: string | undefined; - csrfmiddlewaretoken: string | undefined; -}; - -export const auth_handlers = [ - http.post(resolveURL("/auth/csrf"), () => { - return new HttpResponse(null, { - headers: { - "Set-Cookie": `csrftoken=${mockCSRFToken}; expires=Tue, 31 Dec 2024 23:59:59 GMT; Max-Age=31449600; Path=/; SameSite=Lax`, - }, - }); - }), - - http.post( - resolveURL("/auth/login"), - async ({ request }) => { - const data = await request.json(); - if (data.csrfmiddlewaretoken !== mockCSRFToken) - return new HttpResponse(null, { status: 403 }); - - if (data.username !== mockUsername || data.password !== mockPassword) - return new HttpResponse(null, { status: 403 }); - - return new HttpResponse(null, { - headers: { - "Set-Cookie": `csrftoken=${mockCSRFToken}; expires=Tue, 31 Dec 2024 23:59:59 GMT; Max-Age=31449600; Path=/; SameSite=Lax`, - }, - }); - }, - ), - - http.post( - resolveURL("/auth/logout"), - async ({ request }) => { - const data = await request.json(); - if (data.csrfmiddlewaretoken !== mockCSRFToken) - return new HttpResponse(null, { status: 403 }); - - return new HttpResponse(null, { status: 200 }); - }, - ), -]; diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts deleted file mode 100644 index eb90603..0000000 --- a/src/mocks/handlers.ts +++ /dev/null @@ -1,16 +0,0 @@ -//src/mocks/handlers.ts - -import { http, HttpResponse } from "msw"; -import resolveURL from "../api/fetch.ts"; - -import { auth_handlers } from "./authentication/auth_handlers.ts"; - -const default_handlers = [ - http.get(resolveURL("/resource"), () => { - return HttpResponse.json({ - result: "Hello World!", - }); - }), -]; - -export const handlers = [...default_handlers, ...auth_handlers]; diff --git a/src/mocks/server.ts b/src/mocks/server.ts deleted file mode 100644 index 7b37f2a..0000000 --- a/src/mocks/server.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { setupServer } from "msw/node"; -import { handlers } from "./handlers"; - -export const server = setupServer(...handlers); diff --git a/src/mocks/worker.ts b/src/mocks/worker.ts deleted file mode 100644 index 5d1df39..0000000 --- a/src/mocks/worker.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { setupWorker, SetupWorker } from "msw/browser"; -import { handlers } from "./handlers.ts"; - -export const worker: SetupWorker = setupWorker(...handlers); - -worker.events.on("request:start", ({ request }) => { - console.log("MSW intercepted: ", request.method, request.url); -}); diff --git a/src/routes/router.tsx b/src/routes/router.tsx index 2af295e..74434a2 100644 --- a/src/routes/router.tsx +++ b/src/routes/router.tsx @@ -1,16 +1,6 @@ import { createBrowserRouter } from "react-router-dom"; -import TestPage from "./shared/TestPage.tsx"; -import App from "./App.tsx"; +import routesConfig from "routes/routesConfig.ts"; -const router = createBrowserRouter([ - { - path: "/", - Component: App, - }, - { - path: "/test", - Component: TestPage, - }, -]); +const router = createBrowserRouter(routesConfig); export default router; diff --git a/src/routes/routesConfig.ts b/src/routes/routesConfig.ts new file mode 100644 index 0000000..265bb0f --- /dev/null +++ b/src/routes/routesConfig.ts @@ -0,0 +1,45 @@ +import App from "routes/App.tsx"; +import TestPage from "routes/shared/TestPage.tsx"; +import LoginPage from "routes/shared/LoginPage.tsx"; +import { auth } from "@/firebase"; +import { redirect } from "react-router-dom"; +import { signOut, onAuthStateChanged } from "firebase/auth"; + +const ensureLoggedIn = async () => { + return new Promise((resolve, reject) => { + onAuthStateChanged(auth, (user) => { + if (user) { + resolve({ user }); + } else { + reject(redirect("/login")); + } + }); + }); +}; + +const logOut = async () => { + return await signOut(auth).then(() => redirect("/login")); +}; + +const routesConfig = [ + { + path: "/", + Component: App, + loader: ensureLoggedIn, + }, + { + path: "/test", + Component: TestPage, + loader: ensureLoggedIn, + }, + { + path: "/login", + Component: LoginPage, + }, + { + path: "/logout", + loader: logOut, + } +]; + +export default routesConfig; diff --git a/src/routes/shared/LoginPage.tsx b/src/routes/shared/LoginPage.tsx new file mode 100644 index 0000000..7667bfe --- /dev/null +++ b/src/routes/shared/LoginPage.tsx @@ -0,0 +1,15 @@ +import { useWindowSize } from "@uidotdev/usehooks"; +import {BREAKPOINTS} from "components/constants.tsx"; +import WebLoginContainer from "components/authentication/LoginContainer.tsx"; +import MobileLoginContainer from "components/authentication/MobileLoginContainer.tsx"; + +const LoginPage = () => { + const size = useWindowSize(); + return ( + <> + {size.width && size.width <= BREAKPOINTS.LG ? : } + + ) +} + +export default LoginPage; \ No newline at end of file diff --git a/src/routes/shared/TestPage.tsx b/src/routes/shared/TestPage.tsx index b153aa4..663b2c6 100644 --- a/src/routes/shared/TestPage.tsx +++ b/src/routes/shared/TestPage.tsx @@ -1,20 +1,11 @@ import { useState } from "react"; -import { useQuery } from "@tanstack/react-query"; import viteLogo from "/vite.svg"; import reactLogo from "/react.svg"; -import resolveURL from "@/api/fetch.ts"; - -const getResourceOptions = { - queryKey: ["resourceData"], - queryFn: () => fetch(resolveURL("/resource")).then((res) => res.json()), -}; function TestPage() { const [count, setCount] = useState(0); - const { isPending, data } = useQuery(getResourceOptions); - return ( <>
@@ -25,7 +16,7 @@ function TestPage() { React logo
-

{isPending ? "Loading..." : data.result}

+

Hello World!