Initial commit
This commit is contained in:
commit
2137fcc776
16 changed files with 4057 additions and 0 deletions
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*.tsbuildinfo
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
.DS_Store
|
||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
dist
|
||||
node_modules
|
||||
package-lock.json
|
||||
6
.prettierrc.json
Normal file
6
.prettierrc.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
23
ASSIGNMENT.md
Normal file
23
ASSIGNMENT.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# The Assignment
|
||||
|
||||
Create an image library with the following functionality:
|
||||
|
||||
- Load image links from the Lorem Picsum List Images API.
|
||||
- Display 10 images in equally sized boxes on one page.
|
||||
- Add paging (next/previous) to browse through the images.
|
||||
- Cache API responses during one session (the cache should reset on page refresh).
|
||||
- When an image is clicked, open a modal showing the image in a bigger size.
|
||||
- Bonus (optional): Use any AI tool of your choice to generate a description of the image and display it below the image in the modal.
|
||||
|
||||
# Practical Notes
|
||||
|
||||
- Use React, TypeScript and Material UI.
|
||||
- Functionality and code quality are key, design can be simple.
|
||||
- Please include short instructions on how to run your solution.
|
||||
- Feel free to use any libraries and tools you find useful, including AI.
|
||||
- Upload your code to a git repository and share it with us.
|
||||
|
||||
# Timeline
|
||||
|
||||
- We don’t want this to be perfect nor take that much time of yours. If possible, please try to invest up to 4 hours of your time at maximum.
|
||||
- The deadline is going to be 7 days from now on to make sure you have enough time and space. In case you need more time, just let us know.
|
||||
299
TODO.md
Normal file
299
TODO.md
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
# Tasks
|
||||
|
||||
- understand `picsum` api and what exactly I need from it
|
||||
- understand what exactly is meant by caching (it seems the assignment only wants to cache explicit calls to `picsum`, but there's also image browser caching to consider)
|
||||
- What exactly is Material UI. I assume this is a library of styled components (probably with a React lib available too)
|
||||
- Bonus: Generating API descriptions via AI. This seems much more complex than the rest.
|
||||
|
||||
# Picsum API
|
||||
|
||||
TASK: understand picsum api
|
||||
specifically the `list` endpoint
|
||||
|
||||
Seems that in the background they have this huge list of images,
|
||||
that's stable (i.e. each request given the same input-payload, returns the same list of images).
|
||||
|
||||
They use pages. What's a page?
|
||||
|
||||
So is this like
|
||||
|
||||
```
|
||||
type Pages =
|
||||
List Page
|
||||
|
||||
type Page =
|
||||
List ImageRef
|
||||
```
|
||||
|
||||
Is there some sort of a limit on a given page? Like at-most 30 images per page or something like that?
|
||||
How does that work?
|
||||
|
||||
## Basic Test
|
||||
|
||||
The `https://picsum.photos/200` redirects to e.g. `https://fastly.picsum.photos/id/338/200/200.jpg?hmac=5S5SeR5xW8mbN3Ml7wTTJPePX392JafhcFMGm7IFNy0` which is the image (jpeg).
|
||||
This basically means:
|
||||
|
||||
```
|
||||
{
|
||||
id: 338,
|
||||
width: 200,
|
||||
height: 200,
|
||||
format: "jpg",
|
||||
}
|
||||
```
|
||||
|
||||
it also includes a hash of the payload/image. Why? Apparently this is for CDN caching.
|
||||
|
||||
## Pages
|
||||
|
||||
Ok, set `limit = 10` - that's the page size. Experimentally verified that pages start at `1` not `0`.
|
||||
|
||||
```
|
||||
https://picsum.photos/v2/list?limit=10&page=1
|
||||
```
|
||||
|
||||
This is what's returned in the 3rd page (with `limit=10`).
|
||||
|
||||
```
|
||||
[
|
||||
{
|
||||
"id": "30",
|
||||
"author": "Shyamanta Baruah",
|
||||
"width": 1280,
|
||||
"height": 901,
|
||||
"url": "https://unsplash.com/photos/aeVA-j1y2BY",
|
||||
"download_url": "https://picsum.photos/id/30/1280/901"
|
||||
},
|
||||
{
|
||||
"id": "31",
|
||||
"author": "How-Soon Ngu",
|
||||
"width": 3264,
|
||||
"height": 4912,
|
||||
"url": "https://unsplash.com/photos/7Vz3DtQDT3Q",
|
||||
"download_url": "https://picsum.photos/id/31/3264/4912"
|
||||
},
|
||||
{
|
||||
"id": "32",
|
||||
"author": "Rodrigo Melo",
|
||||
"width": 4032,
|
||||
"height": 3024,
|
||||
"url": "https://unsplash.com/photos/eG3k60PrTGY",
|
||||
"download_url": "https://picsum.photos/id/32/4032/3024"
|
||||
},
|
||||
{
|
||||
"id": "33",
|
||||
"author": "Alejandro Escamilla",
|
||||
"width": 5000,
|
||||
"height": 3333,
|
||||
"url": "https://unsplash.com/photos/LBI7cgq3pbM",
|
||||
"download_url": "https://picsum.photos/id/33/5000/3333"
|
||||
},
|
||||
{
|
||||
"id": "34",
|
||||
"author": "Aleks Dorohovich",
|
||||
"width": 3872,
|
||||
"height": 2592,
|
||||
"url": "https://unsplash.com/photos/zZvsEMPxjIA",
|
||||
"download_url": "https://picsum.photos/id/34/3872/2592"
|
||||
},
|
||||
{
|
||||
"id": "35",
|
||||
"author": "Shane Colella",
|
||||
"width": 2758,
|
||||
"height": 3622,
|
||||
"url": "https://unsplash.com/photos/znM0ujn2RUA",
|
||||
"download_url": "https://picsum.photos/id/35/2758/3622"
|
||||
},
|
||||
{
|
||||
"id": "36",
|
||||
"author": "Vadim Sherbakov",
|
||||
"width": 4179,
|
||||
"height": 2790,
|
||||
"url": "https://unsplash.com/photos/osSryggkso4",
|
||||
"download_url": "https://picsum.photos/id/36/4179/2790"
|
||||
},
|
||||
{
|
||||
"id": "37",
|
||||
"author": "Austin Neill",
|
||||
"width": 2000,
|
||||
"height": 1333,
|
||||
"url": "https://unsplash.com/photos/erTjj730fMk",
|
||||
"download_url": "https://picsum.photos/id/37/2000/1333"
|
||||
},
|
||||
{
|
||||
"id": "38",
|
||||
"author": "Allyson Souza",
|
||||
"width": 1280,
|
||||
"height": 960,
|
||||
"url": "https://unsplash.com/photos/JabLtzJl8bc",
|
||||
"download_url": "https://picsum.photos/id/38/1280/960"
|
||||
},
|
||||
{
|
||||
"id": "39",
|
||||
"author": "Luke Chesser",
|
||||
"width": 3456,
|
||||
"height": 2304,
|
||||
"url": "https://unsplash.com/photos/pFqrYbhIAXs",
|
||||
"download_url": "https://picsum.photos/id/39/3456/2304"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Ok so basically:
|
||||
|
||||
```
|
||||
type ImageId = string // TODO: symbol
|
||||
|
||||
type ImageRef = string
|
||||
|
||||
type Image = {
|
||||
id: ImageId,
|
||||
author: string,
|
||||
width: number,
|
||||
height: number,
|
||||
url: string,
|
||||
download_url: ImageRef
|
||||
}
|
||||
```
|
||||
|
||||
# Caching
|
||||
|
||||
Can I specify sizes better? What about caching?
|
||||
Assignment says that I should cache the requests to the `picsum` api. This is simple enough.
|
||||
I just need to cache the
|
||||
|
||||
```
|
||||
https://picsum.photos/v2/list?limit=10&page=1
|
||||
```
|
||||
|
||||
request. Probably something like `useMemo` would be sufficient (or I can do a custom hook).
|
||||
|
||||
But what about caching of images themselves? Does browser handle this automagically?
|
||||
I'm pretty sure that it does, I definitely thought about this before in itravel. Unfortunately I don't remember the conclusion I reached /facepalm
|
||||
This is just HTTP GET caching, so should be fine.
|
||||
|
||||
To confirm:
|
||||
|
||||
```
|
||||
http --follow --headers https://picsum.photos/id/30/300/200
|
||||
```
|
||||
|
||||
gives
|
||||
|
||||
- First request
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Accept-Ranges: bytes
|
||||
Age: 818206
|
||||
Cache-Control: public, max-age=2592000, stale-while-revalidate=60, stale-if-error=43200, immutable
|
||||
Connection: keep-alive
|
||||
Content-Disposition: inline; filename="30-300x200.jpg"
|
||||
Content-Length: 10181
|
||||
Content-Type: image/jpeg
|
||||
Date: Thu, 14 May 2026 12:48:29 GMT
|
||||
Picsum-Id: 30
|
||||
Server: nginx
|
||||
Timing-Allow-Origin: *
|
||||
Vary: Origin
|
||||
Via: 1.1 varnish
|
||||
X-Cache: HIT
|
||||
X-Cache-Hits: 0
|
||||
X-Served-By: cache-fra-eddf8230047-FRA
|
||||
X-Timer: S1778762910.527297,VS0,VE1
|
||||
```
|
||||
|
||||
- Second request
|
||||
|
||||
```
|
||||
HTTP/1.1 200 OK
|
||||
Accept-Ranges: bytes
|
||||
Age: 818234
|
||||
Cache-Control: public, max-age=2592000, stale-while-revalidate=60, stale-if-error=43200, immutable
|
||||
Connection: keep-alive
|
||||
Content-Disposition: inline; filename="30-300x200.jpg"
|
||||
Content-Length: 10181
|
||||
Content-Type: image/jpeg
|
||||
Date: Thu, 14 May 2026 12:48:57 GMT
|
||||
Picsum-Id: 30
|
||||
Server: nginx
|
||||
Timing-Allow-Origin: *
|
||||
Vary: Origin
|
||||
Via: 1.1 varnish
|
||||
X-Cache: HIT
|
||||
X-Cache-Hits: 1
|
||||
X-Served-By: cache-fra-eddf8230177-FRA
|
||||
X-Timer: S1778762937.389504,VS0,VE1
|
||||
```
|
||||
|
||||
Cache-Control seems to indicate this is cached in CDN.
|
||||
I also need to test the browser behaviour.
|
||||
|
||||
Let's try to test the browser img.src behaviour.
|
||||
|
||||
```
|
||||
var img = new Image();
|
||||
document.body.appendChild(img);
|
||||
```
|
||||
|
||||
Then
|
||||
|
||||
```
|
||||
img.src = "https://picsum.photos/id/30/300/200"; // makes `GET https://picsum.photos/id/30/300/200` which results 302, then `https://fastly.picsum.photos/id/30/300/200.jpg?hmac=VXfU9CUIzgRHYSjKg8FAl7JDQIea3VOfR8f98SpfXbo` which gives 200
|
||||
img.src = "https://picsum.photos/id/31/300/200"; // makes `GET https://picsum.photos/id/31/300/200` which results 302, then `https://fastly.picsum.photos/id/31/300/200.jpg?hmac=WPPS-sLIpuyg7q2io4x82NuBdN-FK1W5uDG3iPVzi2g` which gives 200
|
||||
img.src = "https://picsum.photos/id/30/300/200"; // makes `GET https://picsum.photos/id/30/300/200` which resulst immediately into 200. No further requests are made
|
||||
```
|
||||
|
||||
Cool, as expected browser caches images nicely too.
|
||||
|
||||
# Modal
|
||||
|
||||
"When an image is clicked, open a modal showing the image in a bigger size."
|
||||
Will have to setup basic modal, but that's fine.
|
||||
The point here is that this will just mount a new component (the modal) an in it will be an `<img>` with a different source (the sizes are gonna be different). I don't have to do any manual `fetch`.
|
||||
|
||||
# Material UI
|
||||
|
||||
Yep, it is available as a react library:
|
||||
|
||||
- it even has a modal component `Dialog` which I could use.
|
||||
- `Button` for the prev/next
|
||||
- `CircularProgress` for loading
|
||||
|
||||
Ideally I would love to use something like a fake list of images that are loading when making the `getImages` request to `picsum` api.
|
||||
Need to decide if the current page will just have 10 stable (under prev/next) react components, or will they re-mount from scratch.
|
||||
Yeah, I'm pretty sure these will not remount. What would be the point of that? The images would get stable keys (let's say 1 to 10). But this seems overcomplicated. Let's do something simpler `key={imageId}`. You shouldn't really care about the React remounting all of this. It's not perceptible by humans.
|
||||
When a page is changed, the state that represents them is set to loading (that's when we don't even have the url computed yet).
|
||||
But suppose we do compute the `src` - so under the stable keys the underlying `<img>` DOM node's src will get mutated and either the cached image is gonna be displayed, or GET request is gonna be made by the browser.
|
||||
This is I guess a bit weird... the image is still loading in some sense... Ideally I would prefer to set it to loaded once the real image is loaded for real.
|
||||
|
||||
Fuck it. the states should be
|
||||
|
||||
```
|
||||
| LoadingPage // waiting for the `loadImages`. Here display a "ghost" grid of 10 images.
|
||||
| Loaded {
|
||||
, imageId: List<ImageId>
|
||||
, modal: Option<{ openedImage: ImageId }> // by default modal is not open, but when an image is clicked, it sets this to `some`
|
||||
}
|
||||
```
|
||||
|
||||
# AI generated descriptions
|
||||
|
||||
Now this is a bit weird. I'm kinda sceptical that there's a freely available api that would generate image descriptions.
|
||||
I would need to send the image to the model (I guess there are apis that accept a simple url, I'm pretty sure I don't have to encode the image into binary and send it like that to the model directly).
|
||||
|
||||
Another (insane) option would be to bundle some very simple model in the client (like a wasm image), but I bet anything useful is still like atleast ~500 MB lol.
|
||||
|
||||
Another option would be to self-host it, but that's kinda a lot of work (setting up the server, exposing api, talking to the model locally, self-hosting. Note even sure my minipc could handle any non-trivial model)
|
||||
|
||||
The non-insane option is ofcourse to use some externally hosted model that exposes api endpoint that takes in image url and responds with a description.
|
||||
The problem again is: I shouldn't really deploy that one then. I would have to expose an API key to the public facing internet. Or even have in github /facepalm, which is terrible.
|
||||
But then again, this is just atmost a 4 hour task.
|
||||
I could self-host a tiny api endpoint though on my minipc. But I don't really want to introduce a new server functionality - my pages right now are completely static.
|
||||
|
||||
- The easiest reasonable solution would be to find a free (yeah, right) inference service without needing an API key. Unlikely
|
||||
- Another option would be to get an API key for image-to-text service, and setup a serverless function that would just proxy `image_url` but it would hide the API key.
|
||||
I would hardcode the api endpoint url in github. I think this is fine for this particular assignment.
|
||||
This is probably what I'm gonna use. The only problem I guess could be CORS. When self-hosting
|
||||
TODO: Take a look at cloudflare workers.
|
||||
TODO: Decide which image-to-text model to use.
|
||||
56
eslint.config.js
Normal file
56
eslint.config.js
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import js from '@eslint/js';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
import reactHooks from 'eslint-plugin-react-hooks';
|
||||
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['dist', 'node_modules'],
|
||||
},
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'react-hooks': reactHooks,
|
||||
'react-refresh': reactRefresh,
|
||||
},
|
||||
rules: {
|
||||
...reactHooks.configs.recommended.rules,
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/switch-exhaustiveness-check': 'error',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/no-unnecessary-condition': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'error',
|
||||
'@typescript-eslint/prefer-optional-chain': 'error',
|
||||
'default-case': 'off',
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
},
|
||||
},
|
||||
prettierConfig,
|
||||
);
|
||||
12
index.html
Normal file
12
index.html
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Picturarium</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3495
package-lock.json
generated
Normal file
3495
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
35
package.json
Normal file
35
package.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "picturarium",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"lint": "eslint src",
|
||||
"format": "prettier . --write",
|
||||
"format:check": "prettier . --check",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/material": "^9.0.1",
|
||||
"react": "^19.2.6",
|
||||
"react-dom": "^19.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.6.0",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.59.3",
|
||||
"vite": "^8.0.13"
|
||||
}
|
||||
}
|
||||
12
src/App.tsx
Normal file
12
src/App.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { CssBaseline, Typography } from '@mui/material';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<main>
|
||||
<Typography variant="h1">Hello world</Typography>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
41
src/index.css
Normal file
41
src/index.css
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/* === Reset === */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* === main === */
|
||||
16
src/main.tsx
Normal file
16
src/main.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (rootElement === null) {
|
||||
throw new Error('Root element #root was not found.');
|
||||
}
|
||||
|
||||
createRoot(rootElement).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
11
tmp_repl/tmp_repl.md
Normal file
11
tmp_repl/tmp_repl.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
http --headers https://picsum.photos/200
|
||||
|
||||
http --headers --follow https://picsum.photos/200
|
||||
|
||||
npm run dev
|
||||
|
||||
npm run format
|
||||
|
||||
npm run lint
|
||||
|
||||
npm run build
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitOverride": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
6
vite.config.ts
Normal file
6
vite.config.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue