Added new web app version.

This commit is contained in:
Andrew Lalis 2024-08-26 12:45:19 -04:00
parent 9075938d43
commit 62bc2857d0
34 changed files with 4159 additions and 0 deletions

15
app/.eslintrc.cjs Normal file
View File

@ -0,0 +1,15 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
parserOptions: {
ecmaVersion: 'latest'
}
}

30
app/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

8
app/.prettierrc.json Normal file
View File

@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none"
}

7
app/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}

39
app/README.md Normal file
View File

@ -0,0 +1,39 @@
# app
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```
### Lint with [ESLint](https://eslint.org/)
```sh
npm run lint
```

1
app/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
app/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Andrew Lalis</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3202
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
app/package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "app",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"pinia": "^2.1.7",
"vue": "^3.4.29",
"vue-router": "^4.3.3"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.8.0",
"@tsconfig/node20": "^20.1.4",
"@types/node": "^20.14.5",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^13.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.23.0",
"npm-run-all2": "^6.2.0",
"prettier": "^3.2.5",
"typescript": "~5.4.0",
"vite": "^5.3.1",
"vue-tsc": "^2.0.21"
}
}

BIN
app/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

82
app/src/App.vue Normal file
View File

@ -0,0 +1,82 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import NavLink from './components/NavLink.vue'
</script>
<template>
<div class="bisection-container">
<header class="page-bisection">
<h1 class="site-header-text">Andrew Lalis</h1>
<nav>
<NavLink path="/">Home</NavLink>
<NavLink path="/about">About</NavLink>
<NavLink path="/software">Software</NavLink>
<NavLink path="/gardening">Gardening</NavLink>
<NavLink path="/logbook">Logbook</NavLink>
<NavLink path="/contact">Contact</NavLink>
</nav>
</header>
<RouterView v-slot="{ Component }">
<Transition name="page">
<Component :is="Component" class="page-bisection content-page" />
</Transition>
</RouterView>
</div>
</template>
<style>
.bisection-container {
text-align: center;
}
.page-bisection {
width: 50ch;
display: inline-block;
vertical-align: top;
padding-left: 1rem;
padding-right: 1rem;
}
.content-page {
text-align: left;
margin-top: 1rem;
}
.site-header-text {
font-family: Sacramento;
font-size: 60px;
text-align: right;
margin-top: 1rem;
margin-bottom: 0;
}
/*
Activates once the left and right half of the page stack due to width constraints.
We remove a lot of the vertical spacing to make things fit better.
*/
@media (width < calc(100ch - 2.5rem)) {
.site-header-text {
margin-top: 0;
}
.content-page {
margin-top: 0;
}
}
/*
Once the device width is smaller than one of the halves, sync the page half to the device width.
*/
@media (width < 50ch) {
.page-bisection {
width: calc(100% - 2rem);
}
}
.page-enter-active {
transition: all 0.5s ease;
}
.page-enter-from {
opacity: 0;
transform: translateX(30px);
}
</style>

21
app/src/assets/fonts.css Normal file
View File

@ -0,0 +1,21 @@
@font-face {
font-family: Sacramento;
src: url('@/assets/fonts/Sacramento-Regular.ttf');
}
@font-face {
font-family: BaskervvilleSC;
src: url('@/assets/fonts/BaskervvilleSC-Regular.ttf');
}
@font-face {
font-family: OpenSans;
src: url('@/assets/fonts/OpenSans-VariableFont_wdth,wght.ttf');
font-style: normal;
}
@font-face {
font-family: OpenSans;
src: url('@/assets/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf');
font-style: italic;
}

Binary file not shown.

Binary file not shown.

47
app/src/assets/main.css Normal file
View File

@ -0,0 +1,47 @@
@import '@/assets/fonts.css';
:root {
--text-color: rgb(224, 224, 224);
--background-color: rgb(17, 17, 17);
--background-color-2: rgb(34, 34, 34);
--code-color: rgb(224, 226, 120);
--success-color: rgb(118, 201, 118);
}
body {
margin: 0;
font-family: OpenSans;
font-size: 16px;
color: var(--text-color);
background-color: var(--background-color);
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: BaskervvilleSC;
font-weight: bold;
}
code {
color: var(--code-color);
}
.link-local {
color: var(--success-color);
text-decoration: none;
}
.link-local:hover {
text-decoration: underline;
}
.link-out {
color: var(--code-color);
text-decoration: none;
}
.link-out:hover {
text-decoration: underline;
}

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import type { Message } from '@/views/LogBookView.vue'
import { computed } from 'vue'
const props = defineProps<{
message: Message
}>()
const date = computed(() => {
const b = props.message.createdAt.split(/\D+/)
const offsetMult = props.message.createdAt.indexOf('+') !== -1 ? -1 : 1
const hrOffset = offsetMult * (+b[7] || 0)
const minOffset = offsetMult * (+b[8] || 0)
return new Date(
Date.UTC(+b[0], +b[1] - 1, +b[2], +b[3] + hrOffset, +b[4] + minOffset, +b[5], +b[6] || 0)
)
})
</script>
<template>
<div class="logbook-message">
<p v-text="message.message"></p>
<div>
<small>sent by <span v-text="message.name" style="color: var(--code-color)"></span></small>
</div>
<div>
<small><time :datetime="date.toISOString()" v-text="date.toLocaleString()"></time></small>
</div>
</div>
</template>
<style>
.logbook-message {
background-color: var(--background-color-2);
border-radius: 20px;
padding: 0.75rem;
}
.logbook-message + .logbook-message {
margin-top: 1rem;
}
.logbook-message > time {
font-family: monospace;
}
.logbook-message > p {
margin-top: 0;
margin-bottom: 0.5rem;
}
</style>

View File

@ -0,0 +1,53 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
defineProps<{
path: string
}>()
</script>
<template>
<div class="nav-link">
<RouterLink :to="path" v-bind:class="route.path === path ? 'nav-link-active' : ''">
<slot></slot>
</RouterLink>
</div>
</template>
<style>
.nav-link {
text-align: right;
margin-top: 1rem;
margin-bottom: 1rem;
}
.nav-link > a {
font-family: BaskervvilleSC;
font-size: 30px;
font-weight: bold;
text-decoration: none;
color: inherit;
transition: color 0.25s ease-in-out;
}
.nav-link-active {
color: var(--success-color) !important;
text-decoration: underline !important;
}
.nav-link > a:hover {
color: var(--success-color);
text-decoration: underline;
}
/*
Activates once the left and right half of the page stack due to width constraints.
We remove a lot of the vertical spacing to make things fit better.
*/
@media (width < calc(100ch - 2.5rem)) {
.nav-link {
margin-top: 0;
margin-bottom: 0;
}
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<p class="note">
<slot></slot>
</p>
</template>
<style>
.note {
font-size: small;
font-style: italic;
line-height: 1.2;
}
</style>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{
name: string
url: string
tags?: string[]
}>()
const sortedTags = computed(() => (!props.tags ? null : props.tags.slice().sort()))
const COMMON_TAGS: Record<string, string[]> = {
Java: ['white', '#e76f00'],
Vue: ['white', '#41b883'],
Dlang: ['white', '#b03931'],
Typescript: ['white', '#3178c6'],
Finance: ['white', '#0e4f0a'],
Spring: ['white', '#6db33f']
}
function getTagStyle(tag: string) {
if (tag in COMMON_TAGS) {
return { color: COMMON_TAGS[tag][0], 'background-color': COMMON_TAGS[tag][1] }
}
return {
color: 'white',
'background-color': 'gray'
}
}
</script>
<template>
<div class="software-project-tile">
<h3><a :href="url" v-text="name" class="link-out"></a></h3>
<div class="software-project-tile-content">
<slot></slot>
</div>
<div v-if="tags">
<span
v-for="tag in sortedTags"
:key="tag"
v-text="tag"
class="software-project-tile-tag-badge"
:style="getTagStyle(tag)"
></span>
</div>
</div>
</template>
<style>
.software-project-tile {
background-color: var(--background-color-2);
border-radius: 20px;
padding: 1rem;
}
.software-project-tile + .software-project-tile {
margin-top: 1rem;
}
.software-project-tile > h3 {
margin-top: 0;
margin-bottom: 0.5rem;
font-size: x-large;
}
.software-project-tile-content > p {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
line-height: 1.2;
font-size: 14px;
}
.software-project-tile-tag-badge {
font-size: 12px;
padding-left: 0.5rem;
padding-right: 0.5rem;
padding-top: 0.1rem;
padding-bottom: 0.1rem;
border-radius: 10px;
margin-right: 0.5rem;
}
</style>

14
app/src/main.ts Normal file
View File

@ -0,0 +1,14 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

39
app/src/router/index.ts Normal file
View File

@ -0,0 +1,39 @@
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '@/views/AboutView.vue'
import ContactView from '@/views/ContactView.vue'
import SoftwareView from '@/views/SoftwareView.vue'
import GardeningView from '@/views/GardeningView.vue'
import LogBookView from '@/views/LogBookView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
component: HomeView
},
{
path: '/about',
component: AboutView
},
{
path: '/contact',
component: ContactView
},
{
path: '/software',
component: SoftwareView
},
{
path: '/gardening',
component: GardeningView
},
{
path: '/logbook',
component: LogBookView
}
]
})
export default router

12
app/src/stores/counter.ts Normal file
View File

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

View File

@ -0,0 +1,34 @@
<template>
<main>
<p>
This page was designed and programmed by myself, Andrew, to be my personal presence on the
world-wide-web. I use it as my professional portfolio, in addition to a repository for
anything random that interests me and might be interesting for others too.
</p>
<p>
Feel free to look around, and
<RouterLink to="/contact" class="link-local">contact me</RouterLink> if you find anything
broken or have any questions about anything you find. Maybe say hello in my
<RouterLink to="/logbook" class="link-local">logbook</RouterLink> while you're at it?
</p>
<h3>About This Site, Technically</h3>
<p>
You can find the source code for this website at
<a href="https://git.andrewlalis.com/andrew/homepage" class="link-out">
git.andrewlalis.com/andrew/homepage </a
>. It's built using <a href="https://vuejs.org/" class="link-out">Vue</a>, and styled
completely by hand using plain-old CSS.
</p>
<p>
The site is hosted on a small DigitalOcean "droplet" (their branding for a Virtual Private
Server), and is served through Nginx and various firewalls.
</p>
<p>
If you actually go and view the source code, you'll see a <code>deploy.sh</code> file.
Essentially, all I do is build the site locally, then upload the files. Pretty simple, right?
</p>
</main>
</template>
<style></style>

View File

@ -0,0 +1,35 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
const emailText = ref('')
const emailCorrect = computed(() => {
const testStr = atob('YW5kcmV3bGFsaXNvZmZpY2lhbEBnbWFpbC5jb20=')
return emailText.value.trim().toLowerCase() === testStr
})
</script>
<template>
<main>
<p>
My preferred method of communication is email, which I usually check daily. Send to
<code>first-name + last-name + "official" at gmail dot com</code>.
</p>
<p>
See if you can figure it out:
<input class="contactEmailInput" type="text" v-model="emailText" />
<span v-if="emailCorrect">
<br />
<small style="color: var(--success-color)"><em>You got it!</em></small>
</span>
<br />
<small><em>I write it like that to avoid getting spammed by bots.</em></small>
</p>
<p>My Discord username is <code>____andrew____</code></p>
</main>
</template>
<style>
.contactEmailInput {
font-family: OpenSans, sans-serif;
font-size: 14px;
width: calc(min(30ch, 100%));
}
</style>

View File

@ -0,0 +1,5 @@
<template>
<main>
<p><em>This page is a work in progress. Please check back later.</em></p>
</main>
</template>

View File

@ -0,0 +1,11 @@
<script setup lang="ts"></script>
<template>
<main>
<p>
My name's Andrew, and I'm a software engineer, gardener, runner, home cook, and probably many
other things too, depending on who you ask. Welcome to my website!
</p>
<p>Click one of the links on the left to check out something more interesting.</p>
</main>
</template>

View File

@ -0,0 +1,159 @@
<script setup lang="ts">
import LogbookMessage from '@/components/LogbookMessage.vue'
import NoteText from '@/components/NoteText.vue'
import { onMounted, ref, type Ref } from 'vue'
const LOGBOOK_URL = 'https://logbook.andrewlalis.com'
export interface Message {
createdAt: string
name: string
message: string
}
const messages: Ref<Message[]> = ref([])
const addingMessage = ref(false)
const messageSubmitted = ref(false)
const formName = ref('')
const formMessage = ref('')
onMounted(async () => {
messages.value = await getMessages()
})
async function getMessages(): Promise<Message[]> {
try {
const response = await fetch(LOGBOOK_URL)
if (response.ok) {
return (await response.json()) as Message[]
} else {
console.warn('Failed to get logbook messages.', response.status)
}
} catch (error) {
console.error('Failed to send logbook request.', error)
}
return [] // In case of error, always return an empty list.
}
async function submitMessage() {
const data = {
name: formName.value,
message: formMessage.value
}
try {
const response = await fetch(LOGBOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Accept: 'application/json'
},
body: JSON.stringify(data)
})
if (response.ok) {
messages.value = await getMessages()
formName.value = ''
formMessage.value = ''
addingMessage.value = false
messageSubmitted.value = true
} else {
console.warn('Log message rejected.', response.status)
}
} catch (error) {
console.error('Failed to send log message.', error)
}
}
</script>
<template>
<main>
<p>
If you'd like, you can leave a message in my online logbook so that I and others might see it.
<a
href="#"
v-if="!messageSubmitted"
@click.prevent="addingMessage = !addingMessage"
class="button-link"
>Add your message.</a
>
</p>
<form v-if="addingMessage" class="message-form">
<div class="form-row">
<label for="nameField">Name</label>
<input
id="nameField"
class="message-form-input"
type="text"
name="name"
minlength="2"
maxlength="32"
required
v-model="formName"
/>
</div>
<div class="form-row">
<label for="messageField">Message</label>
<textarea
id="messageField"
class="message-form-input"
type="text"
name="message"
minlength="2"
maxlength="255"
required
v-model="formMessage"
></textarea>
</div>
<div class="form-row">
<a href="#" @click.prevent="submitMessage()" class="button-link">Submit your message!</a>
</div>
<NoteText>
Note that I do check all logs and some basic information about who sent them (IP address,
browser, etc). Inappropriate comments will be removed and their authors banned.
</NoteText>
</form>
<TransitionGroup name="messages" tag="div">
<LogbookMessage v-for="msg in messages" :key="msg.createdAt" :message="msg" />
</TransitionGroup>
</main>
</template>
<style>
.button-link {
color: var(--success-color);
text-decoration: none;
}
.button-link:hover {
text-decoration: underline;
}
.message-form {
margin-bottom: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
.form-row {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.form-row > label {
display: block;
font-weight: bold;
margin-bottom: 0.25rem;
}
.message-form-input {
font-family: OpenSans;
font-size: 16px;
}
#messageField {
width: 100%;
}
.messages-enter-active,
.messages-leave-active {
transition: all 0.5s ease;
}
.messages-enter-from,
.messages-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>

View File

@ -0,0 +1,98 @@
<script setup lang="ts">
import SoftwareProjectTile from '@/components/SoftwareProjectTile.vue'
</script>
<template>
<main>
<p>
Here you'll find a selection of projects I've worked on over the course of my software
engineering career.
</p>
<p>
Beside the projects listed here, you can view my complete portfolio on
<a href="https://git.andrewlalis.com" class="link-out">git.andrewlalis.com</a>. I also have a
<a href="https://github.com/andrewlalis" class="link-out">GitHub</a> account, but I only use
it for collaboration and a projects that rely on GitHub's API integration.
</p>
<SoftwareProjectTile
name="Perfin"
url="https://git.andrewlalis.com/andrew/perfin"
:tags="['Java', 'JavaFX', 'Finance']"
>
<p>
A desktop personal finance application that makes it easy to manage your accounts and
transactions in a secure manner. Record transactions, track brokerage assets, and view
graphs of your spending, total value, and more.
</p>
</SoftwareProjectTile>
<SoftwareProjectTile
name="Guerilla Gardening Map"
url="https://guerilla-gardening.andrewlalis.com"
:tags="['Dlang', 'Vue', 'Typescript']"
>
<p>
A website for anonymously tracking and sharing information on
<a href="https://en.wikipedia.org/wiki/Guerrilla_gardening" class="link-out"
>Guerilla-Gardening</a
>
locations around the world.
</p>
</SoftwareProjectTile>
<SoftwareProjectTile
name="LiteList"
url="https://litelist.andrewlalis.com"
:tags="['Dlang', 'Vue', 'Typescript']"
>
<p>
An extremely barebones web application to demonstrate the viability of my own HTTP server.
It's just your average ToDo list app, with basic JWT-based authentication.
</p>
</SoftwareProjectTile>
<SoftwareProjectTile
name="Handy-Http"
url="https://github.com/andrewlalis/handy-httpd"
:tags="['Dlang']"
>
<p>
An lightweight and flexible HTTP server implemented in the D programming language. Supports
websockets, file serving, complex URL paths with path variables, and more!
</p>
</SoftwareProjectTile>
<SoftwareProjectTile
name="JavaFX Scene Router"
url="https://git.andrewlalis.com/andrew/javafx-scene-router"
:tags="['Java', 'JavaFX']"
>
<p>
A <em>router</em> implementation for JavaFX desktop applications. Define a singleton
<code>SceneRouter</code>, add some routes to it, and add the router's provided view
component to your scene graph. Now you can navigate to different "pages" programmatically or
with user-interaction.
</p>
</SoftwareProjectTile>
<SoftwareProjectTile
name="Gymboard"
url="https://git.andrewlalis.com/andrew/Gymboard"
:tags="['Java', 'Spring', 'Vue', 'Typescript', 'Dlang']"
>
<p>
Proof-of-concept for a local gym leaderboard web application, where users post videos of
their lifts to climb to the top of various leaderboards. Built using a
<em>modular-monolith</em> software architecture.
</p>
</SoftwareProjectTile>
<SoftwareProjectTile
name="Streams"
url="https://github.com/andrewlalis/streams"
:tags="['Dlang']"
>
<p>
A library that defines useful stream primitive types and implementations for IO, which
serves as a convenient wrapper around platform-specific reading and writing operations for
files, sockets, and more.
</p>
</SoftwareProjectTile>
</main>
</template>
<style></style>

14
app/tsconfig.app.json Normal file
View File

@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
app/tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
app/tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node20/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*"
],
"compilerOptions": {
"composite": true,
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

16
app/vite.config.ts Normal file
View File

@ -0,0 +1,16 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})