-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
Playground for increase my skills (@LedruRomane) in ViteJs, React, Typescript and MUI / MUI integration. # [V1] 
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
module.exports = { | ||
root: true, | ||
env: { browser: true, es2020: true }, | ||
extends: [ | ||
'eslint:recommended', | ||
'plugin:@typescript-eslint/recommended', | ||
'plugin:react/recommended', | ||
'plugin:react/jsx-runtime', | ||
'plugin:json/recommended', | ||
'plugin:react-hooks/recommended', | ||
], | ||
ignorePatterns: ['dist', '.eslintrc.cjs'], | ||
parser: '@typescript-eslint/parser', | ||
parserOptions: { | ||
ecmaVersion: 'latest', | ||
sourceType: 'module', | ||
project: ['./tsconfig.json', './tsconfig.node.json'], | ||
tsconfigRootDir: __dirname, | ||
}, | ||
plugins: ['react-refresh'], | ||
settings: { | ||
react: { | ||
version: 'detect', | ||
}, | ||
}, | ||
rules: { | ||
// Basics | ||
'no-unused-vars': 'off', | ||
|
||
// Forbids console.log (prevent accidental commits) | ||
'no-console': ['error', { 'allow': ['debug', 'info', 'warn', 'error'] }], | ||
|
||
// React Refresh | ||
'react-refresh/only-export-components': 'off', | ||
|
||
// React | ||
// https://eslint.org/docs/latest/rules/jsx-quotes | ||
'jsx-quotes': ['error', 'prefer-double'], | ||
// https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-curly-spacing.md | ||
'react/jsx-curly-spacing': ['error', { | ||
when: 'never', | ||
children: true | ||
}], | ||
|
||
// TypeScript | ||
'@typescript-eslint/ban-ts-comment': 'off', | ||
'@typescript-eslint/comma-dangle': ['error', { | ||
// https://eslint.org/docs/latest/rules/comma-dangle#options | ||
arrays: 'always-multiline', | ||
objects: 'always-multiline', | ||
imports: 'always-multiline', | ||
exports: 'always-multiline', | ||
functions: 'always-multiline', | ||
// https://typescript-eslint.io/rules/comma-dangle/ | ||
enums: 'always-multiline', | ||
generics: 'always-multiline', | ||
tuples: 'always-multiline' | ||
}], | ||
|
||
'@typescript-eslint/member-delimiter-style': ['error', { | ||
// https://typescript-eslint.io/rules/member-delimiter-style/ | ||
multiline: { | ||
delimiter: 'none', | ||
requireLast: false | ||
}, | ||
singleline: { | ||
delimiter: 'comma', | ||
requireLast: false | ||
}, | ||
multilineDetection: 'brackets' | ||
}], | ||
|
||
"@typescript-eslint/no-unused-vars": [ | ||
"error" | ||
], | ||
}, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
name: Lint | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
types: [ opened, synchronize, reopened, ready_for_review ] | ||
|
||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
|
||
lint_front: | ||
name: 'Lint Front' | ||
runs-on: ubuntu-latest | ||
timeout-minutes: 15 | ||
# Do not run on WIP or Draft PRs | ||
if: "!github.event.pull_request || (!contains(github.event.pull_request.labels.*.name, 'WIP') && github.event.pull_request.draft == false)" | ||
|
||
steps: | ||
|
||
- name: 'Checkout' | ||
uses: actions/checkout@v3 | ||
|
||
- name: 'Install dependencies' | ||
run: | | ||
make install@integration | ||
- name: 'ESLint front' | ||
run: | | ||
make lint.eslint@integration | ||
- name: 'TypeScript check front' | ||
run: | | ||
make lint.tsc@integration |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,24 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
pnpm-debug.log* | ||
lerna-debug.log* | ||
|
||
node_modules | ||
dist | ||
dist-ssr | ||
*.local | ||
|
||
# Editor directories and files | ||
.vscode/* | ||
!.vscode/extensions.json | ||
.idea | ||
.DS_Store | ||
*.suo | ||
*.ntvs* | ||
*.njsproj | ||
*.sln | ||
*.sw? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
# --------------------------------# | ||
# "make" command | ||
# --------------------------------# | ||
|
||
-include ./make/text.mk | ||
-include ./make/help.mk | ||
-include ./make/url.mk | ||
|
||
.SILENT: | ||
.PHONY: build | ||
|
||
## Setup - Install dependencies | ||
install: | ||
npm install | ||
|
||
install@integration: | ||
npm install --color=always --no-progress --no-audit --no-fund | ||
|
||
## Setup - Update dependencies | ||
update: | ||
npm update | ||
|
||
## Serve - Serve the whole app | ||
serve: export APP_RUNTIME_ENV ?= development | ||
serve: | ||
npx vite --host=mache.ela.ooo --port=63286 | ||
|
||
## Build - Build | ||
build: export APP_RUNTIME_ENV ?= development | ||
build: | ||
npx tsc && npx vite build | ||
|
||
## Tests - Lint | ||
lint: lint.eslint lint.tsc | ||
|
||
lint.eslint: | ||
npx eslint . --fix --ext ts,tsx --report-unused-disable-directives --max-warnings 0 | ||
|
||
lint.tsc: | ||
npx tsc --noemit | ||
|
||
lint@integration: lint.eslint@integration lint.tsc@integration | ||
|
||
lint.eslint@integration: | ||
npx eslint ./ | ||
|
||
lint.tsc@integration: | ||
npx tsc --noemit |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,82 @@ | ||
# Ça-mâche-quoi | ||
ça-mâche-quoi Front | ||
=========== | ||
|
||
Internal project dedicated to the culinary satisfaction of the Elao Magic Team | ||
🍕 🍔 🥙 🥗 🌯 🍣 🍛 🍟 | ||
ça-mâche-quoi front is a React web application in TypeScript. | ||
|
||
<!-- INDEX --> | ||
<details open="open"> | ||
<summary>Index</summary> | ||
<ol> | ||
<li><a href="#setup">Development</a></li> | ||
<li><a href="#needs">Needs</a></li> | ||
</ol> | ||
</details> | ||
## Installation | ||
|
||
```shell | ||
make install | ||
``` | ||
|
||
## Development | ||
|
||
### Setup | ||
Start the dev server / watcher: | ||
|
||
Once you have cloned your project, make sure that you are in the root directory and run this command in your terminal: | ||
```shell | ||
make serve | ||
```` | ||
|
||
Linting: | ||
|
||
```shell | ||
make lint | ||
``` | ||
npm install | ||
``` | ||
|
||
### Usage | ||
## Configuration | ||
|
||
The app exposes some configuration variables through env vars. | ||
By default, it loads vars from env files depending on the context, in the | ||
following order: | ||
|
||
1. `.env` | ||
1. `.env.local` | ||
1. `.env.{production,staging,development}` | ||
1. `.env.{production,staging,development}.local` | ||
|
||
Last defined value wins. Actual env var always wins. | ||
|
||
Once all the necessary dependencies have been installed, your can launch the app by running this command in your terminal: | ||
> **Note**: | ||
> You can also load another specific env file determined by the `ENV_FILE` var, for instance: | ||
> | ||
> ENV_FILE=.env.production make serve | ||
|
||
To add new configuration variables, add these to the main `.env` file | ||
and provide a development value (if relevent) in the `.env.development` file. | ||
|
||
## Build | ||
|
||
Build for production using: | ||
|
||
```shell | ||
make build@production | ||
``` | ||
npm start | ||
|
||
Build for staging using: | ||
|
||
```shell | ||
make build@staging | ||
``` | ||
|
||
## Needs | ||
## Serve | ||
|
||
Serve a build: | ||
|
||
(EN) | ||
- As a user I would like to decide where to eat. | ||
- As a user I would like to select multiple cuisine types to fill my slot machine. | ||
- As a user I would like to select all the cuisinte types when I click on a button. | ||
- As a user I would like to start my slot machine when I click on a button. | ||
- As a user I would like to obtain a result among my options (cuisine type I would like to eat). | ||
- As a user I would like to be able to share the result. | ||
```shell | ||
make serve.static | ||
``` | ||
|
||
(FR) | ||
- En tant qu’utilisateur je veux pouvoir choisir où me restaurer. | ||
- En tant qu’utilisateur je veux pouvoir sélectionner un ensemble de catégories (types de cuisine) pour remplir ma roulette. | ||
- En tant qu'utilisateur je veux pouvoir cocher tout les cases en cliquant sur un bouton | ||
- En tant qu’utilisateur je veux pouvoir déclencher ma roulette en cliquant sur un bouton. | ||
- En tant qu’utilisateur je veux obtenir un résultat (type de cuisine à manger). | ||
- En tant qu'utilisateur je veux pouvoir partager le résultat de ma roulette. | ||
## Going further | ||
|
||
- [Assets](res/doc/assets.md) | ||
- [Routes](res/doc/routes.md) | ||
|
||
## References | ||
|
||
- [TypeScript](https://www.typescriptlang.org/) | ||
- [TypeScript Cheatscheet](https://react-typescript-cheatsheet.netlify.app/) | ||
- [Apollo GraphQL Client](https://www.apollographql.com/docs/react/) | ||
- [Using Apollo with TypeScript](https://www.apollographql.com/docs/react/development-testing/static-typing/) | ||
|
||
--- | ||
⬆︎ [**Back to README.md**](../README.md) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
body { | ||
display: flex; | ||
flex: 1; | ||
height: 100vh; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# React + TypeScript + Vite | ||
|
||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. | ||
|
||
Currently, two official plugins are available: | ||
|
||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh | ||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh | ||
|
||
## Expanding the ESLint configuration | ||
|
||
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: | ||
|
||
- Configure the top-level `parserOptions` property like this: | ||
|
||
```js | ||
export default { | ||
// other rules... | ||
parserOptions: { | ||
ecmaVersion: 'latest', | ||
sourceType: 'module', | ||
project: ['./tsconfig.json', './tsconfig.node.json'], | ||
tsconfigRootDir: __dirname, | ||
}, | ||
} | ||
``` | ||
|
||
- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` | ||
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` | ||
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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>Ça mâche quoi</title> | ||
</head> | ||
<body> | ||
<div id="root"></div> | ||
<script type="module" src="/src/main.tsx"></script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
######## | ||
# Help # | ||
######## | ||
|
||
.DEFAULT_GOAL := help | ||
|
||
MACHE_HELP = \ | ||
Usage: make [$(MACHE_COLOR_INFO)command$(MACHE_COLOR_RESET)] \ | ||
$(call mache_help_section, Help) \ | ||
$(call mache_help,help,This help) | ||
|
||
define mache_help_section | ||
\n\n$(MACHE_COLOR_COMMENT)$(strip $(1)):$(MACHE_COLOR_RESET) | ||
endef | ||
|
||
define mache_help | ||
\n $(MACHE_COLOR_INFO)$(1)$(MACHE_COLOR_RESET) $(2) | ||
endef | ||
|
||
help: | ||
@printf "\n$(MACHE_HELP)" | ||
@awk ' \ | ||
BEGIN { \ | ||
sectionsName[1] = "Commands" ; \ | ||
sectionsCount = 1 ; \ | ||
} \ | ||
/^[-a-zA-Z0-9_.@%\/+]+:/ { \ | ||
if (match(lastLine, /^## (.*)/)) { \ | ||
command = substr($$1, 1, index($$1, ":") - 1) ; \ | ||
section = substr(lastLine, RSTART + 3, index(lastLine, " - ") - 4) ; \ | ||
if (section) { \ | ||
message = substr(lastLine, index(lastLine, " - ") + 3, RLENGTH) ; \ | ||
sectionIndex = 0 ; \ | ||
for (i = 1; i <= sectionsCount; i++) { \ | ||
if (sectionsName[i] == section) { \ | ||
sectionIndex = i ; \ | ||
} \ | ||
} \ | ||
if (!sectionIndex) { \ | ||
sectionIndex = sectionsCount++ + 1 ; \ | ||
sectionsName[sectionIndex] = section ; \ | ||
} \ | ||
} else { \ | ||
message = substr(lastLine, RSTART + 3, RLENGTH) ; \ | ||
sectionIndex = 1 ; \ | ||
} \ | ||
if (length(command) > sectionsCommandLength[sectionIndex]) { \ | ||
sectionsCommandLength[sectionIndex] = length(command) ; \ | ||
} \ | ||
sectionCommandIndex = sectionsCommandCount[sectionIndex]++ + 1; \ | ||
helpsCommand[sectionIndex, sectionCommandIndex] = command ; \ | ||
helpsMessage[sectionIndex, sectionCommandIndex] = message ; \ | ||
} \ | ||
} \ | ||
{ lastLine = $$0 } \ | ||
END { \ | ||
for (i = 1; i <= sectionsCount; i++) { \ | ||
if (sectionsCommandCount[i]) { \ | ||
printf "\n\n$(MACHE_COLOR_COMMENT)%s:$(MACHE_COLOR_RESET)", sectionsName[i] ; \ | ||
for (j = 1; j <= sectionsCommandCount[i]; j++) { \ | ||
printf "\n $(MACHE_COLOR_INFO)%-" sectionsCommandLength[i] "s$(MACHE_COLOR_RESET) %s", helpsCommand[i, j], helpsMessage[i, j] ; \ | ||
} \ | ||
} \ | ||
} \ | ||
} \ | ||
' $(MAKEFILE_LIST) | ||
@printf "\n\n" | ||
@printf "$(if $(MACHE_HELP_PROJECT),$(MACHE_HELP_PROJECT)\n\n)" | ||
.PHONY: help | ||
|
||
help.project: | ||
@printf "$(if $(MACHE_HELP_PROJECT),\n$(MACHE_HELP_PROJECT)\n\n)" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
########## | ||
# Colors # | ||
########## | ||
|
||
MACHE_COLOR_RESET := \033[0m | ||
MACHE_COLOR_ERROR := \033[31m | ||
MACHE_COLOR_INFO := \033[32m | ||
MACHE_COLOR_WARNING := \033[33m | ||
MACHE_COLOR_COMMENT := \033[36m | ||
|
||
###################### | ||
# Special Characters # | ||
###################### | ||
|
||
# Usage: | ||
# $(call mache_message, Foo$(,) bar) = Foo, bar | ||
# $(call mache_message, $(lp)Foo bar) = (Foo bar | ||
# $(call mache_message, Foo$(rp) bar) = Foo) bar | ||
|
||
, := , | ||
lp := ( | ||
rp := ) | ||
|
||
######## | ||
# Time # | ||
######## | ||
|
||
# Usage: | ||
# $(call mache_time) = 11:06:20 | ||
|
||
define mache_time | ||
`date -u +%T` | ||
endef | ||
|
||
########### | ||
# Message # | ||
########### | ||
|
||
# Usage: | ||
# $(call mache_message, Foo bar) = Foo bar | ||
# $(call mache_message_success, Foo bar) = (っ◕‿◕)っ Foo bar | ||
# $(call mache_message_warning, Foo bar) = ¯\_(ツ)_/¯ Foo bar | ||
# $(call mache_message_error, Foo bar) = (╯°□°)╯︵ ┻━┻ Foo bar | ||
|
||
define mache_message | ||
printf "$(MACHE_COLOR_INFO)$(strip $(1))$(MACHE_COLOR_RESET)\n" | ||
endef | ||
|
||
define mache_message_success | ||
printf "$(MACHE_COLOR_INFO)(っ◕‿◕)っ $(strip $(1))$(MACHE_COLOR_RESET)\n" | ||
endef | ||
|
||
define mache_message_warning | ||
printf "$(MACHE_COLOR_WARNING)¯\_(ツ)_/¯ $(strip $(1))$(MACHE_COLOR_RESET)\n" | ||
endef | ||
|
||
define mache_message_error | ||
printf "$(MACHE_COLOR_ERROR)(╯°□°)╯︵ ┻━┻ $(strip $(1))$(MACHE_COLOR_RESET)\n" | ||
endef | ||
|
||
####### | ||
# Log # | ||
####### | ||
|
||
# Usage: | ||
# $(call mache_log, Foo bar) = [11:06:20] [target] Foo bar | ||
# $(call mache_log_warning, Foo bar) = [11:06:20] [target] ¯\_(ツ)_/¯ Foo bar | ||
# $(call mache_log_error, Foo bar) = [11:06:20] [target] (╯°□°)╯︵ ┻━┻ Foo bar | ||
|
||
define mache_log | ||
printf "[$(MACHE_COLOR_COMMENT)$(call mache_time)$(MACHE_COLOR_RESET)] [$(MACHE_COLOR_COMMENT)$(@)$(MACHE_COLOR_RESET)] " ; $(call mache_message, $(1)) | ||
endef | ||
|
||
define mache_log_warning | ||
printf "[$(MACHE_COLOR_COMMENT)$(call mache_time)$(MACHE_COLOR_RESET)] [$(MACHE_COLOR_COMMENT)$(@)$(MACHE_COLOR_RESET)] " ; $(call mache_message_warning, $(1)) | ||
endef | ||
|
||
define mache_log_error | ||
printf "[$(MACHE_COLOR_COMMENT)$(call mache_time)$(MACHE_COLOR_RESET)] [$(MACHE_COLOR_COMMENT)$(@)$(MACHE_COLOR_RESET)] " ; $(call mache_message_error, $(1)) | ||
endef | ||
|
||
########### | ||
# Confirm # | ||
########### | ||
|
||
# Usage: | ||
# $(call mache_confirm, Foo bar) = ༼ つ ◕_◕ ༽つ Foo bar (y/N): | ||
# $(call mache_confirm, Bar foo, y) = ༼ つ ◕_◕ ༽つ Foo bar (Y/n): | ||
|
||
define mache_confirm | ||
$(if $(CONFIRM),, \ | ||
printf "$(MACHE_COLOR_INFO) ༼ つ ◕_◕ ༽つ $(MACHE_COLOR_WARNING)$(strip $(1)) $(MACHE_COLOR_RESET)$(MACHE_COLOR_WARNING)$(if $(filter y,$(2)),(Y/n),(y/N))$(MACHE_COLOR_RESET): " ; \ | ||
read CONFIRM ; \ | ||
case $$CONFIRM in $(if $(filter y,$(2)), \ | ||
[nN]$(rp) printf "\n" ; exit 1 ;; *$(rp) ;;, \ | ||
[yY]$(rp) ;; *$(rp) printf "\n" ; exit 1 ;; \ | ||
) esac \ | ||
) | ||
endef | ||
|
||
################ | ||
# Conditionals # | ||
################ | ||
|
||
# Usage: | ||
# $(call mache_error_if_not, $(FOO), FOO has not been specified) = (╯°□°)╯︵ ┻━┻ FOO has not been specified | ||
|
||
define mache_error_if_not | ||
$(if $(strip $(1)),, \ | ||
$(call mache_message_error, $(strip $(2))) ; exit 1 \ | ||
) | ||
endef | ||
|
||
# Usage: | ||
# $(call mache_confirm_if, $(FOO), Foo bar) = ༼ つ ◕_◕ ༽つ Foo bar (y/N): | ||
|
||
define mache_confirm_if | ||
$(if $(strip $(1)), \ | ||
$(call mache_confirm, $(strip $(2))) | ||
) | ||
endef | ||
|
||
# Usage: | ||
# $(call mache_confirm_if_not, $(FOO), Foo bar) = ༼ つ ◕_◕ ༽つ Foo bar (y/N): | ||
|
||
define mache_confirm_if_not | ||
$(if $(strip $(1)),, \ | ||
$(call mache_confirm, $(strip $(2))) | ||
) | ||
endef | ||
|
||
########## | ||
# Random # | ||
########## | ||
|
||
# Usage: | ||
# $(call mache_rand, 8) = 8th56zp2 | ||
|
||
define mache_rand | ||
`cat /dev/urandom | LC_ALL=C tr -dc 'a-z0-9' | fold -w $(strip $(1)) | head -n 1` | ||
endef |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
######## | ||
# Help # | ||
######## | ||
|
||
MACHE_HELP_PROJECT = $(MACHE_COLOR_COMMENT)┏(°.°)┛┗(°.°)┓$(MACHE_COLOR_RESET) ♪♫ Let's party ♫♪ $(MACHE_COLOR_COMMENT)┗(°.°)┛┏(°.°)┓$(MACHE_COLOR_RESET)\n | ||
MACHE_HELP_PROJECT += $(call mache_help,Front, http://mache.ela.ooo:63286) |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,40 +1,35 @@ | ||
{ | ||
"name": "my-app", | ||
"version": "0.1.0", | ||
"name": "new", | ||
"private": true, | ||
"dependencies": { | ||
"@testing-library/jest-dom": "^5.16.2", | ||
"@testing-library/react": "^12.1.4", | ||
"@testing-library/user-event": "^13.5.0", | ||
"react": "^17.0.2", | ||
"react-dom": "^17.0.2", | ||
"react-router-dom": "^6.3.0", | ||
"react-scripts": "5.0.0", | ||
"sass": "^1.49.9", | ||
"web-vitals": "^2.1.4" | ||
}, | ||
"version": "0.0.0", | ||
"type": "module", | ||
"scripts": { | ||
"start": "react-scripts start", | ||
"build": "react-scripts build", | ||
"test": "react-scripts test", | ||
"eject": "react-scripts eject" | ||
"dev": "vite", | ||
"build": "tsc && vite build", | ||
"lint": "eslint new --ext ts,tsx --report-unused-disable-directives --max-warnings 0", | ||
"preview": "vite preview" | ||
}, | ||
"eslintConfig": { | ||
"extends": [ | ||
"react-app", | ||
"react-app/jest" | ||
] | ||
"dependencies": { | ||
"@emotion/react": "^11.11.3", | ||
"@emotion/styled": "^11.11.0", | ||
"@mui/icons-material": "^5.15.2", | ||
"@mui/material": "^5.15.2", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
}, | ||
"browserslist": { | ||
"production": [ | ||
">0.2%", | ||
"not dead", | ||
"not op_mini all" | ||
], | ||
"development": [ | ||
"last 1 chrome version", | ||
"last 1 firefox version", | ||
"last 1 safari version" | ||
] | ||
"devDependencies": { | ||
"@types/react": "^18.2.46", | ||
"@types/react-dom": "^18.2.17", | ||
"@typescript-eslint/eslint-plugin": "^6.14.0", | ||
"@typescript-eslint/parser": "^6.14.0", | ||
"@vitejs/plugin-react": "^4.2.1", | ||
"eslint": "^8.55.0", | ||
"eslint-plugin-json": "^3.1.0", | ||
"eslint-plugin-react": "^7.33.2", | ||
"eslint-plugin-react-hooks": "^4.6.0", | ||
"eslint-plugin-react-refresh": "^0.4.5", | ||
"sass": "^1.69.6", | ||
"typescript": "^5.2.2", | ||
"vite": "^5.0.8" | ||
} | ||
} |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import '@assets/app.scss' | ||
import AppLayout from '@app/layouts/AppLayout.tsx'; | ||
import { ThemeProvider } from '@mui/material/styles'; | ||
import { CssBaseline } from '@mui/material'; | ||
import { customTheme } from '@app/theme.ts'; | ||
import RestaurantsOptions from '@app/pages/RestaurantsOptions.tsx'; | ||
|
||
function App() { | ||
|
||
return ( | ||
<ThemeProvider theme={customTheme}> | ||
<CssBaseline /> | ||
<AppLayout> | ||
<RestaurantsOptions /> | ||
</AppLayout> | ||
</ThemeProvider> | ||
) | ||
} | ||
|
||
export default App |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { styled } from '@mui/material'; | ||
|
||
export default function Footer() { | ||
return <StyledFooter role="contentinfo"> | ||
Made with 🌮 at elao - © 2024 | ||
</StyledFooter>; | ||
} | ||
|
||
const StyledFooter = styled('footer')(({ theme }) => theme.unstable_sx({ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
alignItems: 'center', | ||
color: theme.palette.common.white, | ||
})); |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import { styled } from '@mui/material'; | ||
|
||
export default function Header() { | ||
|
||
return <StyledHeader> | ||
<h1> | ||
<span>Ça mâche</span> | ||
<span>quoi ?</span> | ||
</h1> | ||
</StyledHeader>; | ||
} | ||
|
||
const StyledHeader = styled('header')(({ theme }) => theme.unstable_sx({ | ||
'& h1': { | ||
margin: '60px 0px 130px 130px', | ||
fontSize: '52px', | ||
color: theme.palette.common.white, | ||
fontFamily: 'Poppins', | ||
|
||
'& span': { | ||
position: 'relative', | ||
zIndex: '1', | ||
|
||
'&:last-of-type': { | ||
top: '59px', | ||
right: '65px', | ||
}, | ||
|
||
'&:before': { | ||
content: '""', | ||
position: 'absolute', | ||
top: '30px', | ||
left: '10px', | ||
width: '100%', | ||
height: '33px', | ||
background: theme.palette.secondary.light, | ||
zIndex: '-1', | ||
}, | ||
}, | ||
}, | ||
})); | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,251 @@ | ||
import { | ||
Box, | ||
Button, | ||
ClickAwayListener, | ||
Grow, | ||
IconButton, | ||
MenuItem, | ||
MenuList, | ||
Paper, | ||
Popper, | ||
styled, | ||
Typography, | ||
} from '@mui/material'; | ||
import MoreVertIcon from '@mui/icons-material/MoreVert'; | ||
import { useRef, useState } from 'react'; | ||
|
||
interface Props { | ||
id: number | ||
name: string | ||
website: string | ||
image: string | ||
pickedRestaurantIds: number[] | ||
setPickedRestaurantIds: (id: number[]) => void | ||
} | ||
|
||
export function RestaurantButton({ | ||
id, | ||
name, | ||
website, | ||
image, | ||
pickedRestaurantIds, | ||
setPickedRestaurantIds, | ||
}: Props) { | ||
const selected = pickedRestaurantIds.includes(id); | ||
const selectedStyle = selected ? | ||
{ | ||
width: '300px', | ||
margin: '20px', | ||
border: '4px solid currentColor', | ||
} : { | ||
width: '300px', | ||
margin: '20px', | ||
}; | ||
const [open, setOpen] = useState(false); | ||
const anchorRef = useRef<HTMLButtonElement>(null); | ||
|
||
function addRestaurantToPool(id: number) { | ||
setPickedRestaurantIds([...pickedRestaurantIds, id]); | ||
} | ||
|
||
function removeRestaurantFromPool(id: number) { | ||
setPickedRestaurantIds(pickedRestaurantIds.filter(restaurantId => id !== restaurantId)); | ||
} | ||
|
||
function toggleRestaurant(value: boolean) { | ||
if (value) { | ||
addRestaurantToPool(id); | ||
} else { | ||
removeRestaurantFromPool(id); | ||
} | ||
} | ||
|
||
function handleClick() { | ||
toggleRestaurant(!selected); | ||
} | ||
|
||
const handleToggle = () => { | ||
setOpen((prevOpen) => !prevOpen); | ||
}; | ||
|
||
const handleClose = (event: Event) => { | ||
if ( | ||
anchorRef.current && | ||
anchorRef.current.contains(event.target as HTMLElement) | ||
) { | ||
return; | ||
} | ||
|
||
setOpen(false); | ||
}; | ||
|
||
const handleMenuItemClick = ( | ||
website: string, | ||
) => { | ||
window.open(website, '_blank'); | ||
setOpen(false); | ||
}; | ||
|
||
return <StyledBox> | ||
<StyledButton | ||
focusRipple | ||
key={name} | ||
style={selectedStyle} | ||
onClick={handleClick} | ||
> | ||
<ImageSrc style={{ backgroundImage: `url(${image})` }} /> | ||
<ImageBackdrop className="MuiImageBackdrop-root" /> | ||
<Image> | ||
<Typography | ||
component="span" | ||
variant="subtitle1" | ||
color="inherit" | ||
sx={{ | ||
position: 'relative', | ||
p: 4, | ||
pt: 2, | ||
pb: (theme) => `calc(${theme.spacing(1)} + 6px)`, | ||
}} | ||
> | ||
{name} | ||
<ImageMarked className="MuiImageMarked-root" /> | ||
</Typography> | ||
</Image> | ||
</StyledButton> | ||
<StyledIconButton | ||
color="primary" | ||
aria-label="See website" | ||
aria-controls={open ? 'split-button-menu' : undefined} | ||
aria-expanded={open ? 'true' : undefined} | ||
aria-haspopup="menu" | ||
onClick={handleToggle} | ||
ref={anchorRef} | ||
opened={open} | ||
> | ||
<MoreVertIcon /> | ||
</StyledIconButton> | ||
<Popper | ||
sx={{ | ||
zIndex: 1, | ||
}} | ||
open={open} | ||
anchorEl={anchorRef.current} | ||
role={undefined} | ||
transition | ||
disablePortal | ||
placement="bottom-start" | ||
> | ||
{({ TransitionProps }) => ( | ||
<Grow | ||
{...TransitionProps} | ||
> | ||
<Paper> | ||
<ClickAwayListener onClickAway={handleClose}> | ||
<StyledMenuList id="split-button-menu" autoFocusItem> | ||
<StyledMenuItem | ||
onClick={() => handleMenuItemClick(website)} | ||
> | ||
Visit website | ||
</StyledMenuItem> | ||
</StyledMenuList> | ||
</ClickAwayListener> | ||
</Paper> | ||
</Grow> | ||
)} | ||
</Popper> | ||
</StyledBox> | ||
} | ||
|
||
const StyledMenuList = styled(MenuList)(({ theme }) => ({ | ||
color: theme.palette.text.primary, | ||
backgroundColor: theme.palette.primary.main, | ||
marginTop: '-20px', | ||
})); | ||
|
||
const StyledMenuItem = styled(MenuItem)(({ theme }) => ({ | ||
color: theme.palette.secondary.contrastText, | ||
'&:hover': { | ||
color: theme.palette.common.white, | ||
}, | ||
})); | ||
|
||
const StyledIconButton = styled(IconButton)<{ | ||
opened: boolean | ||
}>(({ theme, opened }) => ({ | ||
color: theme.palette.common.white, | ||
backgroundColor: theme.palette.primary.main, | ||
width: '40px', | ||
height: '40px', | ||
position: 'absolute', | ||
zIndex: 2, | ||
opacity: opened ? 0 : 1, | ||
})); | ||
|
||
const StyledBox = styled(Box)({ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
}); | ||
|
||
const StyledButton = styled(Button)(({ theme }) => ({ | ||
position: 'relative', | ||
height: 200, | ||
[theme.breakpoints.down('sm')]: { | ||
width: '100% !important', // Overrides inline-style | ||
height: 100, | ||
}, | ||
'&:hover, &.Mui-focusVisible': { | ||
zIndex: 1, | ||
'& .MuiImageBackdrop-root': { | ||
opacity: 0.15, | ||
}, | ||
'& .MuiImageMarked-root': { | ||
opacity: 0, | ||
}, | ||
'& .MuiTypography-root': { | ||
border: '4px solid currentColor', | ||
}, | ||
}, | ||
})); | ||
|
||
const Image = styled('span')(({ theme }) => ({ | ||
position: 'absolute', | ||
left: 0, | ||
right: 0, | ||
top: 0, | ||
bottom: 0, | ||
display: 'flex', | ||
alignItems: 'center', | ||
justifyContent: 'center', | ||
color: theme.palette.common.white, | ||
})); | ||
|
||
const ImageBackdrop = styled('span')(({ theme }) => ({ | ||
position: 'absolute', | ||
left: 0, | ||
right: 0, | ||
top: 0, | ||
bottom: 0, | ||
backgroundColor: theme.palette.common.black, | ||
opacity: 0.4, | ||
transition: theme.transitions.create('opacity'), | ||
})); | ||
|
||
const ImageMarked = styled('span')(({ theme }) => ({ | ||
height: 3, | ||
width: 18, | ||
backgroundColor: theme.palette.common.white, | ||
position: 'absolute', | ||
bottom: -2, | ||
left: 'calc(50% - 9px)', | ||
transition: theme.transitions.create('opacity'), | ||
})); | ||
|
||
const ImageSrc = styled('span')({ | ||
position: 'absolute', | ||
left: 0, | ||
right: 0, | ||
top: 0, | ||
bottom: 0, | ||
backgroundSize: 'cover', | ||
backgroundPosition: 'center 40%', | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Fab, styled } from '@mui/material' | ||
|
||
const FabStyled = styled(Fab)(({ theme }) => ({ | ||
position: 'fixed', | ||
right: 0, | ||
backgroundColor: theme.palette.primary.main, | ||
'&:hover': { | ||
backgroundColor: theme.palette.primary.light, | ||
}, | ||
})); | ||
|
||
export default function ValidateButton() { | ||
return ( | ||
<FabStyled> | ||
Valider | ||
</FabStyled> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { Button, Card, CardActions, CardContent, CardMedia, Typography } from '@mui/material'; | ||
import ToggleSwitch from '@app/components/UI/ToggleSwitch.tsx'; | ||
|
||
|
||
interface Props { | ||
id: number | ||
name: string | ||
description: string | ||
website: string | ||
image: string | ||
pickedRestaurantIds: number[] | ||
setPickedRestaurantIds: (id: number[]) => void | ||
} | ||
|
||
export function RestaurantCard({ | ||
id, | ||
name, | ||
description, | ||
website, | ||
image, | ||
pickedRestaurantIds, | ||
setPickedRestaurantIds, | ||
}: Props) { | ||
|
||
function addRestaurantToPool (id: number) { | ||
setPickedRestaurantIds([...pickedRestaurantIds, id]); | ||
} | ||
|
||
function removeRestaurantFromPool (id: number) { | ||
setPickedRestaurantIds(pickedRestaurantIds.filter(restaurantId => id !== restaurantId)); | ||
} | ||
|
||
const selected = pickedRestaurantIds.includes(id); | ||
|
||
function toggleRestaurant (value: boolean) { | ||
if (value) { | ||
addRestaurantToPool(id); | ||
} else { | ||
removeRestaurantFromPool(id); | ||
} | ||
} | ||
|
||
return <> | ||
<Card sx={{ maxWidth: 345 }}> | ||
<CardMedia | ||
sx={{ height: 140 }} | ||
component="img" | ||
image={image} | ||
alt={name} | ||
/> | ||
<CardContent> | ||
<Typography gutterBottom variant="h5" component="div"> | ||
{name} | ||
</Typography> | ||
<Typography variant="body2" color="text.secondary"> | ||
{description} | ||
</Typography> | ||
</CardContent> | ||
<CardActions> | ||
<ToggleSwitch label="Ajouter au pool de selection" checked={selected} setChecked={toggleRestaurant} /> | ||
<Button size="small" href={website} target="_blank">Website</Button> | ||
</CardActions> | ||
</Card> | ||
</> | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import React from 'react' | ||
import { FormGroup, FormControlLabel, Switch, styled } from '@mui/material' | ||
|
||
interface Props { | ||
label: string | ||
checked: boolean | ||
setChecked: (value: boolean) => void | ||
} | ||
|
||
export default function ToggleSwitch({ | ||
label, | ||
checked, | ||
setChecked, | ||
}: Props) { | ||
|
||
const handleSwitch = (event: React.ChangeEvent<HTMLInputElement>) => { | ||
setChecked(event.target.checked); | ||
} | ||
|
||
return ( | ||
<FormGroup | ||
style={{ | ||
marginBottom: '20px', | ||
}} | ||
> | ||
<LabelStyled | ||
control={ | ||
<Switch checked={checked} onChange={handleSwitch} /> | ||
} | ||
label={label} | ||
/> | ||
</FormGroup> | ||
); | ||
} | ||
|
||
|
||
const LabelStyled = styled(FormControlLabel)(({ theme }) => ({ | ||
color: theme.palette.secondary.contrastText, | ||
'& .MuiSwitch-track': { | ||
backgroundColor: theme.palette.secondary.contrastText, | ||
}, | ||
})); |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
const data = { | ||
"restaurants": [ | ||
{ | ||
"id": 1, | ||
"name": "Restaurant 1", | ||
"website": "https://mui.com/material-ui/react-card/", | ||
"image": "https://mui.com/static/images/cards/contemplative-reptile.jpg", | ||
}, | ||
{ | ||
"id": 2, | ||
"name": "Restaurant 2", | ||
"website": "https://mui.com/material-ui/react-card/", | ||
"image": "https://mui.com/static/images/cards/contemplative-reptile.jpg", | ||
}, | ||
{ | ||
"id": 3, | ||
"name": "Restaurant 3", | ||
"website": "https://mui.com/material-ui/react-card/", | ||
"image": "https://mui.com/static/images/cards/contemplative-reptile.jpg", | ||
}, | ||
], | ||
}; | ||
|
||
export default data; |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { PropsWithChildren } from 'react'; | ||
import Header from '@app/components/Header/Header.tsx'; | ||
import Footer from '@app/components/Footer/Footer.tsx'; | ||
import { Container, styled } from '@mui/material'; | ||
import background from '@images/background.svg'; | ||
|
||
export default function AppLayout({ children }: PropsWithChildren) { | ||
return <StyledContainer> | ||
<Header /> | ||
<main> | ||
{children} | ||
</main> | ||
<Footer /> | ||
</StyledContainer>; | ||
} | ||
|
||
|
||
const StyledContainer = styled(Container)(({ theme }) => theme.unstable_sx({ | ||
display: 'flex', | ||
flexDirection: 'column', | ||
minHeight: '100vh', | ||
minWidth: '100vw', | ||
main: { | ||
flex: 1, | ||
}, | ||
backgroundImage: `url(${background})`, | ||
backgroundRepeat: 'no-repeat', | ||
backgroundPosition: 'right bottom', | ||
backgroundSize: 'auto 100%', | ||
backgroundColor: theme.palette.secondary.main, | ||
})) |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import React from 'react' | ||
import ReactDOM from 'react-dom/client' | ||
import App from '@app/App.tsx' | ||
import '@assets/app.scss'; | ||
|
||
ReactDOM.createRoot(document.getElementById('root')!).render( | ||
<React.StrictMode> | ||
<App /> | ||
</React.StrictMode>, | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { Box, styled } from '@mui/material'; | ||
import { useEffect, useState } from 'react'; | ||
import data from '@app/data/restaurants.ts'; | ||
import { RestaurantButton } from '@app/components/UI/Button/RestaurantButton.tsx'; | ||
import ToggleSwitch from '@app/components/UI/ToggleSwitch.tsx'; | ||
import ValidateButton from '@app/components/UI/Button/ValidateButton.tsx'; | ||
|
||
export interface Restaurant { | ||
id: number | ||
name: string | ||
website: string | ||
image: string | ||
} | ||
|
||
export default function RestaurantsOptions() { | ||
const [pickedRestaurantIds, setPickedRestaurantIds] = useState<number[]>([]); | ||
const [allSelected, setAllSelected] = useState<boolean>(false); | ||
|
||
const dataRestaurants = data.restaurants; | ||
|
||
useEffect(() => { | ||
if (allSelected) { | ||
setPickedRestaurantIds(dataRestaurants.map(restaurant => restaurant.id)); | ||
} else { | ||
setPickedRestaurantIds([]); | ||
} | ||
},[allSelected, dataRestaurants]); | ||
|
||
return <> | ||
<ToggleSwitch label="Tout sélectionner" checked={allSelected} setChecked={setAllSelected}/> | ||
<StyledContainer> | ||
<ValidateButton/> | ||
{dataRestaurants && dataRestaurants.map((restaurant: Restaurant) => { | ||
return <RestaurantButton | ||
key={restaurant.id} | ||
id={restaurant.id} | ||
name={restaurant.name} | ||
website={restaurant.website} | ||
image={restaurant.image} | ||
pickedRestaurantIds={pickedRestaurantIds} | ||
setPickedRestaurantIds={setPickedRestaurantIds} | ||
/> | ||
})} | ||
</StyledContainer> | ||
</> | ||
} | ||
|
||
const StyledContainer = styled(Box)(({ theme }) => theme.unstable_sx({ | ||
display: 'flex', | ||
})); | ||
|
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { createTheme } from '@mui/material'; | ||
import PoppinsRegular from '@assets/fonts/poppins-regular.woff2'; | ||
import PoppinsBold from '@assets/fonts/poppins-bold.woff2'; | ||
import PoppinsSemiBold from '@assets/fonts/poppins-semibold.woff2'; | ||
|
||
const poppinsRegular = { | ||
fontFamily: 'Poppins', | ||
fontStyle: 'normal', | ||
fontDisplay: 'swap', | ||
fontWeight: 400, | ||
src: `url(${PoppinsRegular}) format('woff2')`, | ||
} | ||
|
||
const poppinsBold = { | ||
fontFamily: 'Poppins', | ||
fontStyle: 'normal', | ||
fontDisplay: 'swap', | ||
fontWeight: 700, | ||
src: `url(${PoppinsBold}) format('woff2')`, | ||
} | ||
|
||
const poppinsSemiBold = { | ||
fontFamily: 'Poppins', | ||
fontStyle: 'normal', | ||
fontDisplay: 'swap', | ||
fontWeight: 600, | ||
src: `url(${PoppinsSemiBold}) format('woff2')`, | ||
} | ||
|
||
const theme = createTheme({ | ||
typography: { | ||
fontFamily: [ | ||
'Poppins', | ||
'sans-serif', | ||
].join(','), | ||
}, | ||
components: { | ||
MuiCssBaseline: { | ||
styleOverrides: { | ||
html: [ | ||
{'@font-face': poppinsRegular}, | ||
{'@font-face': poppinsSemiBold}, | ||
{'@font-face': poppinsBold}, | ||
], | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
export const customTheme = createTheme({ | ||
...theme, | ||
palette: { | ||
primary: { | ||
main: '#5135d1', | ||
dark: '#3e2ca1', | ||
light: '#bdb3ec', | ||
contrastText: '#181A41', | ||
}, | ||
secondary: { | ||
main: '#181a41', | ||
dark: '#2b2371', | ||
light: '#3e2ca1', | ||
contrastText: '#f3f2f9', | ||
}, | ||
common: { | ||
white: '#FFFFFF', | ||
black: '#0C0D24', | ||
}, | ||
}, | ||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/// <reference types="vite/client" /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
{ | ||
"compilerOptions": { | ||
"baseUrl": "./", | ||
"paths": { | ||
"@app/*": [ | ||
"src/*" | ||
], | ||
"@assets/*": [ | ||
"assets/*" | ||
], | ||
"@images/*": [ | ||
"assets/images/*" | ||
], | ||
"@styles/*": [ | ||
"assets/styles/*" | ||
], | ||
}, | ||
"target": "ES2020", | ||
"useDefineForClassFields": true, | ||
"lib": ["ES2020", "DOM", "DOM.Iterable"], | ||
"module": "ESNext", | ||
"skipLibCheck": true, | ||
|
||
/* Bundler mode */ | ||
"moduleResolution": "bundler", | ||
"allowImportingTsExtensions": true, | ||
"resolveJsonModule": true, | ||
"isolatedModules": true, | ||
"noEmit": true, | ||
"jsx": "react-jsx", | ||
|
||
/* Linting */ | ||
"strict": true, | ||
"noUnusedLocals": true, | ||
"noUnusedParameters": true, | ||
"noFallthroughCasesInSwitch": true, | ||
|
||
// https://www.typescriptlang.org/tsconfig#strictNullChecks | ||
"strictNullChecks": true | ||
}, | ||
"include": ["src"], | ||
"references": [{ "path": "./tsconfig.node.json" }] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
{ | ||
"compilerOptions": { | ||
"composite": true, | ||
"skipLibCheck": true, | ||
"module": "ESNext", | ||
"moduleResolution": "bundler", | ||
"allowSyntheticDefaultImports": true | ||
}, | ||
"include": [ | ||
"vite.config.ts" | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { defineConfig } from 'vite' | ||
import react from '@vitejs/plugin-react' | ||
|
||
// https://vitejs.dev/config/ | ||
export default defineConfig({ | ||
plugins: [ | ||
react(), | ||
], | ||
resolve: { | ||
alias: { | ||
'@app': '/src', | ||
'@assets': '/assets', | ||
'@images': '/assets/images', | ||
'@scss': '/assets/styles', | ||
}, | ||
}, | ||
server: { | ||
host: '0.0.0.0', | ||
port: 63286, | ||
strictPort: true, | ||
}, | ||
}) |