Compare commits
No commits in common. "main" and "v1.1.0" have entirely different histories.
@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} build
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-go:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -21,21 +21,3 @@ jobs:
|
||||
run: cd admin && go get .
|
||||
- name: "[admin] Build"
|
||||
run: cd admin && go build -v ./...
|
||||
build-nuxt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Bun 1.2.0
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.0
|
||||
- name: Display Bun version
|
||||
run: bun --version
|
||||
- name: test 1
|
||||
run: ls
|
||||
- name: test 2
|
||||
run: cd web && ls
|
||||
- name: "[web] Install dependencies"
|
||||
run: cd web && bun install
|
||||
- name: "[web] Build"
|
||||
run: cd web && bun run build
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -85,6 +85,8 @@ __pycache__/
|
||||
|
||||
## Node
|
||||
node_modules
|
||||
package-lock.json
|
||||
package.json
|
||||
/src/doc/rustc-dev-guide/mermaid.min.js
|
||||
|
||||
## Rustdoc GUI tests
|
||||
@ -102,4 +104,3 @@ flake.lock
|
||||
# Before adding new lines, see the comment at the top.
|
||||
|
||||
.env*
|
||||
.idea/
|
||||
|
@ -59,7 +59,7 @@ func makeGetReq(url string) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
req.Header.Set("Authorization", token)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -84,8 +84,7 @@ func makePostReq(url string, body any) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", token)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -120,8 +119,8 @@ func main() {
|
||||
Action: test,
|
||||
},
|
||||
{
|
||||
Name: "open",
|
||||
Usage: "get door state (opened or closed)",
|
||||
Name: "get",
|
||||
Usage: "get door state",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "raw",
|
||||
@ -201,7 +200,7 @@ func getOpened(ctx context.Context, cmd *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
opened, err := makeGetReq(fmt.Sprintf("%s/open", server))
|
||||
opened, err := makeGetReq(fmt.Sprintf("%s/read", server))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -245,7 +244,7 @@ func getAlert(context.Context, *cli.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
alert, err := makeGetReq(fmt.Sprintf("%s/alert", server))
|
||||
alert, err := makeGetReq(fmt.Sprintf("%s/alerts", server))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -306,7 +305,7 @@ func createManageAlert(alert bool) func(context.Context, *cli.Command) error {
|
||||
data.For = secs
|
||||
}
|
||||
|
||||
_, err = makePostReq(fmt.Sprintf("%s/alert", server), data)
|
||||
_, err = makePostReq(fmt.Sprintf("%s/alerts", server), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
2
peripheral/.gitignore
vendored
2
peripheral/.gitignore
vendored
@ -1,3 +1 @@
|
||||
__pycache__/
|
||||
|
||||
.env.json
|
||||
|
@ -1,4 +1,3 @@
|
||||
import gc
|
||||
import utime
|
||||
import ujson
|
||||
import network
|
||||
@ -8,6 +7,7 @@ from machine import Pin
|
||||
THRESHOLD_DISTANCE = 15 # cm
|
||||
SOUND_SPEED = 340 * 100 # m/s * centi = cm/s
|
||||
MAX_CONNECTION_RETRIES = 50
|
||||
ULTRA_OPENED_THRESHOLD = 3
|
||||
|
||||
|
||||
class App:
|
||||
@ -18,7 +18,7 @@ class App:
|
||||
|
||||
opened: bool = False
|
||||
previously_opened: bool = False
|
||||
wlan: network.WLAN
|
||||
ultra_opened_counter: int = 0
|
||||
|
||||
led = Pin(15, Pin.OUT)
|
||||
trigger = Pin(2, Pin.OUT)
|
||||
@ -57,18 +57,18 @@ class App:
|
||||
|
||||
def connect(self):
|
||||
print("Connecting to Wi-Fi...")
|
||||
self.wlan = network.WLAN(network.STA_IF)
|
||||
wlan = network.WLAN(network.STA_IF)
|
||||
|
||||
self.wlan.active(False)
|
||||
wlan.active(False)
|
||||
utime.sleep_ms(250)
|
||||
self.wlan.active(True)
|
||||
wlan.active(True)
|
||||
utime.sleep_ms(250)
|
||||
|
||||
self.wlan.connect(self.ssid, self.password)
|
||||
wlan.connect(self.ssid, self.password)
|
||||
utime.sleep_ms(100)
|
||||
|
||||
retry_count = 0
|
||||
while not self.wlan.isconnected():
|
||||
while not wlan.isconnected():
|
||||
if retry_count >= MAX_CONNECTION_RETRIES:
|
||||
print("Max connection retries reached")
|
||||
exit(1)
|
||||
@ -82,12 +82,12 @@ class App:
|
||||
|
||||
if retry_count % 10 == 0:
|
||||
print("Attempting to restart connection...")
|
||||
self.wlan.connect(self.ssid, self.password)
|
||||
wlan.connect(self.ssid, self.password)
|
||||
for _ in range(10):
|
||||
self.led.toggle()
|
||||
utime.sleep_ms(50)
|
||||
|
||||
print(f"Connected with IP {self.wlan.ifconfig()[0]}")
|
||||
print(f"Connected with IP {wlan.ifconfig()[0]}")
|
||||
self.update_server()
|
||||
|
||||
def update_server(self):
|
||||
@ -101,36 +101,28 @@ class App:
|
||||
headers={"Authorization": self.token, "Content-Type": "application/json"},
|
||||
data=raw
|
||||
)
|
||||
print(f"State updated [{r.status_code}] {r.content.decode()}")
|
||||
print(f"State updated [{r.status_code}]")
|
||||
except Exception as e:
|
||||
print(f"Error occurred: {e}")
|
||||
|
||||
def measure_distance(self) -> float:
|
||||
def measure_distance(self):
|
||||
self.trigger.low()
|
||||
utime.sleep_us(2)
|
||||
self.trigger.high()
|
||||
utime.sleep_us(10)
|
||||
self.trigger.low()
|
||||
|
||||
start_time = utime.ticks_us()
|
||||
sent_time = utime.ticks_us()
|
||||
while self.echo.value() == 0:
|
||||
print("hey", end="\r")
|
||||
sent_time = utime.ticks_us()
|
||||
if sent_time - start_time >= 100_000: # if it takes more than 100ms, stop
|
||||
return -1
|
||||
|
||||
start_time = utime.ticks_us()
|
||||
received_time = utime.ticks_us()
|
||||
while self.echo.value() == 1:
|
||||
print("ho", end="\r")
|
||||
received_time = utime.ticks_us()
|
||||
if received_time - start_time >= 100_000: # same
|
||||
return -1
|
||||
|
||||
delta_time = received_time - sent_time
|
||||
distance = delta_time / 1_000_000 * SOUND_SPEED / 2
|
||||
print(f"Distance: {distance:0<5} cm; mem_free = {gc.mem_free()}")
|
||||
print(f"Distance: {distance} cm")
|
||||
|
||||
return distance
|
||||
|
||||
@ -141,43 +133,36 @@ class App:
|
||||
i = 0
|
||||
while True:
|
||||
try:
|
||||
if not self.wlan.isconnected():
|
||||
self.connect()
|
||||
|
||||
distance = self.measure_distance()
|
||||
self.opened = distance >= THRESHOLD_DISTANCE
|
||||
|
||||
if not self.opened:
|
||||
if not opened:
|
||||
self.led.low()
|
||||
self.ultra_opened_counter = 0
|
||||
|
||||
# was it just closed?
|
||||
if self.previously_opened:
|
||||
self.update_server()
|
||||
self.previously_opened = False
|
||||
else:
|
||||
# was it just opened? +wait delay
|
||||
if not self.previously_opened:
|
||||
self.led.high()
|
||||
self.led.high()
|
||||
self.ultra_opened_counter += 1
|
||||
|
||||
if self.ultra_opened_counter >= ULTRA_OPENED_THRESHOLD:
|
||||
self.update_server()
|
||||
self.previously_opened = True
|
||||
|
||||
if i >= 20:
|
||||
print("Blinking...")
|
||||
self.led.toggle()
|
||||
utime.sleep_ms(100)
|
||||
self.led.toggle()
|
||||
utime.sleep_ms(400)
|
||||
|
||||
i = 0
|
||||
gc.collect()
|
||||
else:
|
||||
utime.sleep_ms(500)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fatal error occurred: {e}")
|
||||
print(f"Fatal exception occurred: {e}")
|
||||
|
||||
self.previously_opened = self.opened
|
||||
i += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App().run()
|
||||
|
@ -73,10 +73,6 @@ func main() {
|
||||
LogLevel: log.ERROR,
|
||||
}))
|
||||
app.Use(middleware.Secure())
|
||||
app.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
|
||||
}))
|
||||
|
||||
app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: "${time_custom} ${method} ${uri} ---> ${status} in ${latency_human} (${bytes_out} bytes)\n",
|
||||
@ -134,6 +130,7 @@ func main() {
|
||||
}
|
||||
|
||||
if data.Opened == opened {
|
||||
mut.Unlock()
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
@ -217,8 +214,7 @@ func main() {
|
||||
|
||||
func authed(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
provided := c.Request().Header.Get("Authorization")
|
||||
if provided != fmt.Sprintf("Bearer %s", token) {
|
||||
if c.Request().Header.Get("Authorization") != token {
|
||||
return c.NoContent(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
|
@ -1,10 +0,0 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
node_modules/
|
||||
.output/
|
||||
.data/
|
||||
.nuxt/
|
||||
.cache/
|
||||
|
||||
.env*
|
24
web/.gitignore
vendored
24
web/.gitignore
vendored
@ -1,24 +0,0 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
@ -1,6 +0,0 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": false
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
FROM oven/bun:1 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock .
|
||||
RUN bun install --frozen-lockfile --production
|
||||
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
FROM oven/bun:1
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/.output .
|
||||
|
||||
ENV PORT 3000
|
||||
ENV HOST 0.0.0.0
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["bun", "/app/server/index.mjs"]
|
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import "assets/css/main.css";
|
||||
</style>
|
@ -1,2 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
2027
web/bun.lock
2027
web/bun.lock
File diff suppressed because it is too large
Load Diff
@ -1,42 +0,0 @@
|
||||
import type { FetchResponse } from "ofetch"
|
||||
import { AxiosError, type AxiosResponse } from "axios"
|
||||
|
||||
export function useAPI(route: string = "/") {
|
||||
return `https://api.door.svitan.dev${route}`
|
||||
}
|
||||
|
||||
export async function handleRequestError(error: unknown) {
|
||||
const toast = useToast()
|
||||
let message = undefined
|
||||
if (error instanceof Error) {
|
||||
message = error.message
|
||||
}
|
||||
|
||||
toast.add({
|
||||
title: "Error occurred",
|
||||
description: message,
|
||||
color: "error",
|
||||
})
|
||||
}
|
||||
|
||||
export function handleResponse<T extends { toString(): string }>(
|
||||
response: AxiosResponse<T>,
|
||||
success: (response: AxiosResponse<T>) => void = () => {}
|
||||
) {
|
||||
const token = useToken()
|
||||
const toast = useToast()
|
||||
|
||||
if (response.status === 200) {
|
||||
success(response)
|
||||
} else if (response.status === 401) {
|
||||
toast.add({ title: "Token not valid", color: "error" })
|
||||
token.value = ""
|
||||
navigateTo("/token")
|
||||
} else {
|
||||
toast.add({
|
||||
title: "Error occurred",
|
||||
description: response.data.toString(),
|
||||
color: "error",
|
||||
})
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
export function useToken() {
|
||||
return useCookie<string | undefined>("token")
|
||||
}
|
||||
|
||||
export function useHeaders(override: string | undefined = undefined) {
|
||||
const token = useToken()
|
||||
return {
|
||||
Authorization: `Bearer ${override ?? token.value}`,
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-05-15",
|
||||
devtools: { enabled: true },
|
||||
modules: ["@nuxt/icon", "@nuxt/ui", "@nuxtjs/tailwindcss"],
|
||||
});
|
@ -1,26 +0,0 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/icon": "1.13.0",
|
||||
"@nuxt/ui": "3.1.3",
|
||||
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
|
||||
"axios": "^1.9.0",
|
||||
"nuxt": "^3.17.5",
|
||||
"typescript": "^5.6.3",
|
||||
"valibot": "^1.1.0",
|
||||
"vue": "^3.5.16",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
<template>
|
||||
<main
|
||||
class="flex items-center justify-center min-w-screen min-h-screen gap-4 flex-wrap"
|
||||
>
|
||||
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
|
||||
<div
|
||||
:class="`flex items-center justify-center border-solid border-2 border-${open ? (locked ? 'error' : 'secondary') : 'primary'} rounded-lg w-full h-32`"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
open
|
||||
? 'material-symbols:door-open'
|
||||
: 'material-symbols:door-front'
|
||||
"
|
||||
class="size-12 !w-16 !h-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p
|
||||
:class="`flex items-center justify-center text-${open ? (locked ? 'error' : 'secondary') : 'primary'} text-xl w-full h-10`"
|
||||
>
|
||||
{{ open ? "Opened" : "Closed" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
|
||||
<div
|
||||
:class="`flex items-center justify-center border-solid border-2 border-${locked ? 'primary' : 'secondary'} rounded-lg w-full h-32`"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
locked
|
||||
? 'material-symbols:lock'
|
||||
: 'material-symbols:lock-open'
|
||||
"
|
||||
class="size-12 !w-16 !h-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
:icon="
|
||||
locked
|
||||
? 'material-symbols:lock-open'
|
||||
: 'material-symbols:lock'
|
||||
"
|
||||
size="xl"
|
||||
:color="locked ? 'primary' : 'secondary'"
|
||||
variant="solid"
|
||||
class="flex items-center justify-center w-full h-10"
|
||||
@click="toggleLock"
|
||||
>
|
||||
{{ locked ? "Unlock" : "Lock" }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
|
||||
<div
|
||||
:class="`flex items-center justify-center border-solid border-2 border-${alert ? 'primary' : 'secondary'} rounded-lg w-full h-32`"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
alert
|
||||
? 'material-symbols:volume-up'
|
||||
: 'material-symbols:volume-off'
|
||||
"
|
||||
class="size-12 !w-16 !h-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
:icon="
|
||||
alert
|
||||
? 'material-symbols:volume-off'
|
||||
: 'material-symbols:volume-up'
|
||||
"
|
||||
size="xl"
|
||||
:color="alert ? 'primary' : 'secondary'"
|
||||
variant="solid"
|
||||
class="flex items-center justify-center w-full h-10"
|
||||
@click="() => (alert = !alert)"
|
||||
>
|
||||
{{ alert ? "Turn off" : "Turn on" }}
|
||||
</UButton>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import axios from "axios"
|
||||
|
||||
const token = useToken()
|
||||
|
||||
const open = ref(true)
|
||||
const locked = ref(true)
|
||||
const alert = ref(false)
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
async function toggleLock() {
|
||||
const desired = !locked.value
|
||||
axios
|
||||
.post(
|
||||
useAPI("/lock"),
|
||||
{
|
||||
locked: desired,
|
||||
},
|
||||
{
|
||||
headers: useHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
handleResponse(res, () => {
|
||||
toast.add({
|
||||
title: `The door was ${desired ? "locked" : "unlocked"}`,
|
||||
})
|
||||
locked.value = desired
|
||||
})
|
||||
})
|
||||
.catch(handleRequestError)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!token.value) {
|
||||
return navigateTo("/token")
|
||||
}
|
||||
|
||||
axios
|
||||
.get<boolean>(useAPI("/lock"), {
|
||||
headers: useHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
handleResponse(res, (response) => {
|
||||
locked.value = response.data
|
||||
})
|
||||
})
|
||||
.catch(handleRequestError)
|
||||
})
|
||||
</script>
|
@ -1,60 +0,0 @@
|
||||
<template>
|
||||
<main class="flex items-center justify-center min-w-screen min-h-screen">
|
||||
<UForm
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit="onSubmit"
|
||||
class="flex flex-col items-end justify-center gap-y-2"
|
||||
>
|
||||
<UFormField label="Token" name="token" size="xl" required>
|
||||
<UInput
|
||||
v-model="state.token"
|
||||
placeholder="Your token..."
|
||||
size="xl"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" size="xl"> Submit </UButton>
|
||||
</UForm>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as v from "valibot"
|
||||
import type { FormSubmitEvent } from "@nuxt/ui"
|
||||
import axios from "axios"
|
||||
|
||||
const token = useToken()
|
||||
|
||||
const schema = v.object({
|
||||
token: v.pipe(v.string(), v.nonEmpty("Please enter your token")),
|
||||
})
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
const state = reactive({
|
||||
token: "",
|
||||
})
|
||||
const toast = useToast()
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
const received = event.data.token
|
||||
axios
|
||||
.get<boolean>(useAPI("/open"), {
|
||||
headers: useHeaders(received),
|
||||
})
|
||||
.then((res) => {
|
||||
handleResponse(res, () => {
|
||||
toast.add({ title: "Token saved", color: "success" })
|
||||
token.value = received
|
||||
navigateTo("/")
|
||||
})
|
||||
})
|
||||
.catch(handleRequestError)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (token.value) {
|
||||
return navigateTo("/")
|
||||
}
|
||||
})
|
||||
</script>
|
Binary file not shown.
Before Width: | Height: | Size: 264 KiB |
@ -1,2 +0,0 @@
|
||||
User-Agent: *
|
||||
Disallow: /
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
purge: [],
|
||||
darkMode: "class", // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
@ -1,4 +0,0 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user