diff --git a/devNotes.md b/devNotes.md
index ef9d6b5..2eb6377 100644
--- a/devNotes.md
+++ b/devNotes.md
@@ -1 +1,97 @@
# Developer Notes
+
+**Login Credentials:**
+For demonstration purposes, authentication uses a static email and password:
+- **Email:** `user@example.com`
+- **Password:** `password123`
+
+## Tech Stack
+- **Framework:** Vue 3 + Vite
+- **Styling:** SCSS with variables in `_variables.scss`
+- **Routing:** Vue Router
+- **State Management:** Local component state
+- **API Layer:** `apiService.ts` for data fetching abstraction
+- **Icons:** Installed **FontAwesome** package for icons such as close, save, logout, create new record, actions, and pagination buttons.
+
+## Flow Overview
+
+### 1. SignIn Page
+- Simulated authentication using `localStorage` flag (`isAuthenticated`).
+- **Validation:** Ensures both email and password fields are non-empty.
+- If either email or password is invalid, a **pop-up modal** appears saying *"Email or password is incorrect"*.
+- On successful login:
+ - A `loggedIn` key with a value of `true` is stored in **localStorage**.
+ - User is redirected to **Dashboard**.
+- **Auto Logout:** After 15 minutes of inactivity, the user is automatically logged out, redirected back to Sign In, and must log in again.
+
+### 2. Dashboard
+- Fetches initial client data from static `clients.json` via `apiService.ts`.
+- Displays data using the **`TableList`** component.
+- **Table Columns:**
+ - **Actions** (dropdown with View, Edit, Delete)
+ - **Name**
+ - **Company**
+ - **Subscription Cost** (with currency displayed beside the value)
+ - **Age**
+- **Pagination:**
+ - Implemented with `<`, `<<`, numbered pages (`1`, `2`, ...), `>>`, and `>` buttons.
+ - **Per page limit:** 10 records.
+ - If there are more than 10 records, remaining records are shown on additional pages.
+ - Blue pagination buttons indicate they are clickable.
+ - **Record Ordering:** Newly created records are inserted **at the top** of the list (most recent first), so they are visible immediately on the first page.
+ - This ordering is handled by sorting records in descending order based on `id`.
+- **Buttons:**
+ - **Logout:** Logs the user out and redirects to Sign In.
+ - **+ Create new record:** Opens `ModalForm` for adding new records.
+- **Actions Column Details:**
+ - Implemented as a dropdown with **View**, **Edit**, and **Delete** options:
+ - **View:** Reuses the same modal structure to display:
+ - Profile picture
+ - Name
+ - Company
+ - Subscription cost
+ - Currency
+ - Age (hidden if not provided)
+ - Gender (hidden if not provided)
+ - Date registered (formatted as `"May 12, 2004"`)
+ - Modal can be closed.
+ - **Edit:** Reuses the create record modal but runs edit logic to update the existing record instead.
+ - **Delete:** Shows a confirmation modal asking the admin to confirm deletion and explicitly stating the client’s name to prevent accidental deletions.
+
+### 3. ModalForm
+- Reusable modal for creation of new client records.
+- Emits submitted data to parent (`Dashboard.vue`) for table update.
+- **Validation Rules:**
+ - Required: **name, company, subscription cost, currency**.
+ - An asterisk (`*`) is displayed beside required fields.
+ - **Subscription cost** and **age** must be **≥ 0**.
+ - **Currency:** Uses a dropdown list populated via the `currency-codes` package. User can type to auto-search and set the currency for quick navigation and reduced errors.
+ - **Optional fields:** Age and Gender (Gender uses an enum — `0` for male, `1` for female).
+ - **Save button** is disabled unless all required fields are filled.
+- **User Actions in Modal:**
+ - Save record (if valid).
+ - Cancel creation.
+ - Close modal.
+
+## Validation Rules Summary
+- **SignIn.vue:**
+ - Required: email, password.
+ - Invalid email/password → pop-up modal error message.
+- **ModalForm.vue:**
+ - Required: name, company, subscription cost, currency.
+ - Optional: age, gender.
+ - No negative values for subscription cost or age.
+ - Currency selection via searchable dropdown (currency-codes package).
+ - Save disabled unless required fields are complete.
+
+## File Responsibilities
+- **`main.ts`**: Application entry, mounts Vue instance, registers router & styles.
+- **`router.ts`**: Defines routes (`/signin`, `/dashboard`) with guards.
+- **`apiService.ts`**: Encapsulates API calls to fetch data.
+- **`record.ts`**: TypeScript interfaces/types for records.
+- **`SignIn.vue`**: Login form & authentication logic.
+- **`Dashboard.vue`**: Main authenticated page, data fetching & management.
+- **`TableList.vue`**: Generic table rendering component with dropdown actions, delete confirmation modal, and pagination with **latest-first sorting**.
+- **`ModalForm.vue`**: Modal popup for record creation with validation.
+- **`main.scss`**: Global styles.
+- **`_variables.scss`**: Style variables for theme consistency.
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..9c463d9
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ CMS
+
+
+
+
+
+
diff --git a/package.json b/package.json
index bf9fa05..df4873c 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,29 @@
{
- "name": "virtusize-assignment",
- "version": "1.0.0",
- "description": "A frontend coding challenge for Virtusize Frontend team",
- "main": "index.js",
+ "name": "frontend-assignment",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
"scripts": {
- "start": "",
- "api": "json-server --watch clients.json --port 4090",
- "test": ""
+ "dev": "vite",
+ "build": "vue-tsc -b && vite build",
+ "preview": "vite preview",
+ "api": "json-server --watch clients.json --port 3000"
},
- "author": "virtusize",
- "license": "UNLICENSED",
"dependencies": {
- "json-server": "^1.0.0-beta.2"
+ "@fortawesome/fontawesome-svg-core": "^7.0.0",
+ "@fortawesome/free-solid-svg-icons": "^7.0.0",
+ "@fortawesome/vue-fontawesome": "^3.1.1",
+ "currency-codes": "^2.2.0",
+ "vue": "^3.5.18",
+ "vue-router": "^4.5.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^6.0.1",
+ "@vue/tsconfig": "^0.7.0",
+ "json-server": "^1.0.0-beta.3",
+ "sass": "^1.90.0",
+ "typescript": "~5.8.3",
+ "vite": "^7.1.0",
+ "vue-tsc": "^3.0.5"
}
}
diff --git a/public/icon.svg b/public/icon.svg
new file mode 100644
index 0000000..4dba767
--- /dev/null
+++ b/public/icon.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/App.vue b/src/App.vue
new file mode 100644
index 0000000..f857a6e
--- /dev/null
+++ b/src/App.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/src/apiService.ts b/src/apiService.ts
new file mode 100644
index 0000000..91a26e9
--- /dev/null
+++ b/src/apiService.ts
@@ -0,0 +1,52 @@
+import type { RecordItem } from "./record";
+
+const API_URL = 'http://localhost:3000';
+
+async function request(endpoint: string, options: RequestInit = {}): Promise {
+ const res = await fetch(`${API_URL}${endpoint}`, {
+ headers: { 'Content-Type': 'application/json' },
+ ...options
+ });
+
+ if (!res.ok) {
+ throw new Error(`HTTP error! Status: ${res.status}`);
+ }
+
+ return res.json();
+}
+
+export async function getRecords(): Promise {
+ return request('/clients');
+}
+
+export async function getRecordedById(id: number): Promise {
+ return request(`/clients/${id}`);
+}
+
+export async function createRecord(record: any): Promise {
+ return request(`/clients`, {
+ method: 'POST',
+ body: JSON.stringify(record)
+ });
+}
+
+export async function updateRecord(id: number, record: any): Promise {
+ return request(`/clients/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(record)
+ });
+}
+
+export async function deleteRecord(id: number): Promise {
+ return request(`/clients/${id}`, {
+ method: 'DELETE'
+ });
+}
+
+export default {
+ getRecords,
+ getRecordedById,
+ createRecord,
+ updateRecord,
+ deleteRecord
+};
diff --git a/src/components/ModalForm.vue b/src/components/ModalForm.vue
new file mode 100644
index 0000000..13c0f32
--- /dev/null
+++ b/src/components/ModalForm.vue
@@ -0,0 +1,516 @@
+
+
+
+
+
+
+
diff --git a/src/components/TableList.vue b/src/components/TableList.vue
new file mode 100644
index 0000000..da42451
--- /dev/null
+++ b/src/components/TableList.vue
@@ -0,0 +1,270 @@
+
+
+
+
+
+
+ Actions
+ Name
+ Company
+ Subscription Cost
+ Age
+
+
+
+
+
+
+
+
+ Actions
+
+
+
+
+
+ {{ record.name }}
+ {{ record.company }}
+ {{ record.subscriptionCost + ' ' + record.currency.toUpperCase() }}
+ {{ record.age ?? '-' }}
+
+
+
+
+
+
+
+
+
Confirm Delete
+
Are you sure you want to delete {{ recordToDelete?.name }} ?
+
+ Yes
+ No
+
+
+
+
+
+
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..3014b28
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,13 @@
+import { createApp } from 'vue'
+import App from './App.vue'
+import router from './router.ts'
+import './styles/main.scss'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
+import { faCog, faPlus, faChevronDown, faAnglesLeft, faAngleLeft, faAngleRight, faAnglesRight, faTimes, faSignOutAlt, faSave } from '@fortawesome/free-solid-svg-icons'
+
+library.add(faCog, faPlus, faChevronDown, faAnglesLeft, faAngleLeft, faAngleRight, faAnglesRight, faTimes, faSignOutAlt, faSave)
+
+const app = createApp(App)
+app.component('font-awesome-icon', FontAwesomeIcon)
+app.use(router).mount('#app')
diff --git a/src/record.ts b/src/record.ts
new file mode 100644
index 0000000..f1c0ddb
--- /dev/null
+++ b/src/record.ts
@@ -0,0 +1,16 @@
+export interface RecordItem {
+ id: number;
+ gender: Gender | null | '';
+ name: string;
+ company: string;
+ age: number | null;
+ picture?: string;
+ registered?: string;
+ currency: string;
+ subscriptionCost: number;
+}
+
+export enum Gender {
+ Male = 0,
+ Female = 1
+}
diff --git a/src/router.ts b/src/router.ts
new file mode 100644
index 0000000..627c5f5
--- /dev/null
+++ b/src/router.ts
@@ -0,0 +1,28 @@
+import { createWebHistory, createRouter, type RouteRecordRaw } from "vue-router";
+import Dashboard from "./views/Dashboard.vue";
+import SignIn from "./views/SignIn.vue";
+
+const routes: RouteRecordRaw[] = [
+ { path: '/', redirect: 'signin' },
+ { path: '/signin', name: 'SignIn', component: SignIn },
+ { path: '/dashboard', name: 'Dashboard', component: Dashboard, meta: { requiresAuth: true } }
+];
+
+const router = createRouter({
+ history: createWebHistory(),
+ routes
+});
+
+router.beforeEach((to, from, next) => {
+ const loggedIn = localStorage.getItem('loggedIn') === 'true';
+
+ if (to.meta.requiresAuth && !loggedIn) {
+ next('/signin');
+ } else if (to.path === '/signin' && loggedIn) {
+ next('/dashboard');
+ } else {
+ next();
+ }
+});
+
+export default router;
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
new file mode 100644
index 0000000..b7c484d
--- /dev/null
+++ b/src/styles/_variables.scss
@@ -0,0 +1,61 @@
+$primary: #007bff; // blue
+$primary-dark: #0056b3; // darker blue for hover
+$primary-darker: #004494; // even darker blue for focus outline
+
+$secondary: #e0e0e0; // gray light
+$secondary-hover: #cacaca; // gray hover
+
+$danger: #dc3545; // red
+$danger-hover: #b1271b; // darker red for hover
+
+$background-color: #f5f7fa; // page background
+$background-white: #fff;
+
+$text-color: #333;
+$text-color-dark: #555;
+$text-color-light: #777;
+
+$border-color: #ccc;
+$border-light: #ddd;
+$border-lighter: #f1f1f1;
+
+$modal-overlay-bg: rgba(0, 0, 0, 0.4);
+
+$input-focus-color: $primary; // blue
+$input-focus-shadow: rgba(0, 123, 255, 0.4); // semi-transparent blue
+
+$dropdown-hover-bg: #f0f0f0;
+
+$nav-border: #eee;
+
+:root {
+ --primary: #{$primary};
+ --primary-dark: #{$primary-dark};
+ --primary-darker: #{$primary-darker};
+
+ --secondary: #{$secondary};
+ --secondary-hover: #{$secondary-hover};
+
+ --danger: #{$danger};
+ --danger-hover: #{$danger-hover};
+
+ --background-color: #{$background-color};
+ --background-white: #{$background-white};
+
+ --text-color: #{$text-color};
+ --text-color-dark: #{$text-color-dark};
+ --text-color-light: #{$text-color-light};
+
+ --border-color: #{$border-color};
+ --border-light: #{$border-light};
+ --border-lighter: #{$border-lighter};
+
+ --modal-overlay-bg: #{$modal-overlay-bg};
+
+ --input-focus-color: #{$input-focus-color};
+ --input-focus-shadow: #{$input-focus-shadow};
+
+ --dropdown-hover-bg: #{$dropdown-hover-bg};
+
+ --nav-border: #{$nav-border};
+}
diff --git a/src/styles/main.scss b/src/styles/main.scss
new file mode 100644
index 0000000..1b9617a
--- /dev/null
+++ b/src/styles/main.scss
@@ -0,0 +1,396 @@
+@use './variables' as vars;
+@use "sass:color";
+
+/* Reset and base body styling */
+body {
+ font-family: 'Inter', sans-serif;
+ background-color: vars.$background-color;
+ margin: 0;
+ padding: 5rem;
+ color: vars.$text-color;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+/* Container helper */
+.container {
+ max-width: 900px;
+ margin: auto;
+ padding: 1rem;
+}
+
+/* Table styles */
+.table {
+ width: 100%;
+ border-collapse: collapse;
+ background: vars.$background-white;
+ border-radius: 8px;
+ margin-bottom: 2rem;
+
+ th {
+ background: #f8f9fa;
+ padding: 0.6rem;
+ text-align: left;
+ font-weight: 600;
+ border-bottom: 1px solid vars.$border-light;
+ }
+
+ td {
+ padding: 0.6rem;
+ border-bottom: 1px solid vars.$border-lighter;
+ }
+
+ tr:hover {
+ background: #f6f8fa;
+ }
+}
+
+/* Base button styles */
+.button {
+ background-color: vars.$primary;
+ color: vars.$background-white;
+ padding: 0.4rem 1rem;
+ font-size: 0.95rem;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ min-width: 80px;
+ text-align: center;
+ user-select: none;
+ transition: background-color 0.3s ease, box-shadow 0.2s ease;
+
+ &:hover:not(:disabled) {
+ background-color: vars.$primary-dark;
+ box-shadow: 0 0 8px #{color.adjust(vars.$primary, $alpha: 0.4)};
+ }
+
+ &:focus-visible {
+ outline: 2px solid vars.$primary-darker;
+ outline-offset: 2px;
+ }
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.6;
+ pointer-events: none;
+ }
+}
+
+/* Button modifiers */
+.btn-primary {
+ @extend .button;
+ background-color: vars.$primary;
+
+ &:hover:not(:disabled) {
+ background-color: vars.$primary-dark;
+ }
+}
+
+.btn-secondary {
+ @extend .button;
+ background-color: vars.$secondary;
+ color: vars.$text-color;
+
+ &:hover:not(:disabled) {
+ background-color: vars.$secondary-hover;
+ }
+}
+
+.btn-danger {
+ @extend .button;
+ background-color: vars.$danger;
+ color: vars.$background-white;
+
+ &:hover:not(:disabled) {
+ background-color: color.adjust(vars.$danger, $lightness: -10%);
+ }
+}
+
+/* Navigation bar */
+nav {
+ background: vars.$background-white;
+ padding: 1rem;
+ border-bottom: 1px solid vars.$nav-border;
+
+ a {
+ text-decoration: none;
+ margin-right: 1rem;
+ color: vars.$primary;
+
+ &:hover,
+ &:focus-visible {
+ text-decoration: underline;
+ outline: none;
+ }
+ }
+}
+
+/* Dropdown menus */
+.dropdown {
+ position: relative;
+ display: inline-block;
+}
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ background: vars.$background-white;
+ border: 1px solid vars.$border-light;
+ border-radius: 4px;
+ list-style: none;
+ margin: 0;
+ padding: 0.25rem 0;
+ width: 100px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ z-index: 10;
+
+ li {
+ padding: 0.5rem 1rem;
+ cursor: pointer;
+ font-size: 0.9rem;
+
+ &:hover,
+ &:focus-visible {
+ background-color: vars.$dropdown-hover-bg;
+ outline: none;
+ }
+ }
+}
+
+/* Pagination buttons */
+.pagination-button {
+ width: 1.8rem;
+ height: 1.8rem;
+ padding: 0;
+ border-radius: 4px;
+ border: 1px solid vars.$border-color;
+ background: vars.$background-white;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ font-size: 0.9rem;
+ line-height: 1;
+
+ &:disabled {
+ cursor: not-allowed;
+ opacity: 0.5;
+ }
+
+ &:not(:disabled):hover,
+ &:not(:disabled):focus-visible {
+ background-color: vars.$dropdown-hover-bg;
+ outline: none;
+ }
+}
+
+.pagination-button.active {
+ background-color: vars.$primary;
+ color: vars.$background-white;
+ border-color: vars.$primary;
+}
+
+/* Modal overlay and content */
+.modal-overlay {
+ position: fixed;
+ top: 0; left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: vars.$modal-overlay-bg;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ padding: 1rem;
+ overflow-y: auto;
+}
+
+.modal-content {
+ background: vars.$background-white;
+ padding: 1.5rem 2rem;
+ border-radius: 8px;
+ max-width: 400px;
+ width: 90%;
+ text-align: center;
+ box-shadow: 0 2px 12px rgba(0,0,0,0.3);
+ max-height: 90vh;
+ overflow-y: auto;
+ box-sizing: border-box;
+}
+
+/* Modal buttons reuse global button classes for consistency */
+.modal-close-btn,
+.btn-danger,
+.btn-secondary {
+ padding: 0.5rem 1.25rem;
+ border: none;
+ font-weight: 600;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.3s ease;
+ user-select: none;
+}
+
+.modal-close-btn,
+.btn-danger {
+ background-color: vars.$danger;
+ color: vars.$background-white;
+
+ &:hover,
+ &:focus-visible {
+ background-color: vars.$danger-hover;
+ outline: none;
+ }
+}
+
+.btn-secondary {
+ background-color: #888;
+ color: vars.$background-white;
+
+ &:hover,
+ &:focus-visible {
+ background-color: #666;
+ outline: none;
+ }
+}
+
+/* Form inputs and selects */
+input,
+select,
+textarea {
+ width: 100%;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid vars.$border-color;
+ border-radius: 6px;
+ font-size: 1rem;
+ transition: border-color 0.3s ease, box-shadow 0.3s ease;
+ box-sizing: border-box;
+
+ &:focus {
+ outline: none;
+ border-color: vars.$input-focus-color;
+ box-shadow: 0 0 6px vars.$input-focus-shadow;
+ }
+
+ &[readonly],
+ &[disabled] {
+ background-color: #f5f5f5;
+ cursor: not-allowed;
+ color: vars.$text-color-light;
+ }
+}
+
+input[readonly],
+select[disabled] {
+ background-color: vars.$background-white !important;
+ color: vars.$text-color !important;
+ opacity: 1 !important;
+ cursor: default !important;
+}
+
+input[readonly][required]:invalid {
+ box-shadow: none;
+ outline: none;
+}
+
+/* Utility class to remove select arrows when readonly */
+.select-no-arrow {
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ padding-right: 1em;
+ background-image: none !important;
+ cursor: default;
+}
+
+/* Validation error text */
+.validation-error {
+ color: #ef4444;
+ font-size: 0.75rem;
+ margin-top: 0.5rem;
+ margin-bottom: 0;
+}
+
+/* Client picture styling for profile images */
+.client-picture {
+ display: flex;
+ justify-content: center;
+
+ img {
+ width: 100%;
+ max-width: 160px;
+ height: auto;
+ aspect-ratio: 1 / 1;
+ object-fit: cover;
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
+ background-color: #f0f0f0;
+ text-align: center;
+ line-height: 160px;
+ }
+}
+
+/* Close button for modals */
+.close-btn {
+ background: transparent;
+ border: none;
+ font-size: 1.5rem;
+ line-height: 1;
+ cursor: pointer;
+ color: vars.$text-color-dark;
+ transition: color 0.3s ease;
+ padding: 0;
+ margin: 0;
+
+ &:hover,
+ &:focus-visible {
+ color: vars.$input-focus-color;
+ outline: none;
+ }
+
+ &:focus-visible {
+ outline: 2px solid vars.$input-focus-color;
+ outline-offset: 2px;
+ }
+}
+
+/* Animations */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+@keyframes slideUp {
+ from {
+ transform: translateY(20px);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Responsive adjustments */
+@media (max-width: 400px) {
+ .modal-content {
+ padding: 1.5rem;
+ }
+
+ .modal-title {
+ font-size: 1.25rem;
+ }
+
+ .button {
+ font-size: 0.9rem;
+ padding: 0.4rem 1rem;
+ max-width: 100px;
+ }
+}
diff --git a/src/views/Dashboard.vue b/src/views/Dashboard.vue
new file mode 100644
index 0000000..d4746c9
--- /dev/null
+++ b/src/views/Dashboard.vue
@@ -0,0 +1,255 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Confirm Logout
+
Are you sure you want to logout?
+
+ Yes
+ No
+
+
+
+
+
+
+
+
+ Logged Out
+
+
You have been logged out due to inactivity.
+
OK
+
+
+
+
+
+
diff --git a/src/views/SignIn.vue b/src/views/SignIn.vue
new file mode 100644
index 0000000..7ba92f3
--- /dev/null
+++ b/src/views/SignIn.vue
@@ -0,0 +1,193 @@
+
+
+
+
+
Sign In
+
+
+
+
+
+
+
Invalid Credentials
+
The email or password you entered is incorrect.
+
Close
+
+
+
+
+
+
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..80fafe8
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1,7 @@
+///
+
+declare module '*.vue' {
+ import { DefineComponent } from "vue";
+ const component: DefineComponent<{}, {}, any>
+ export default component
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..bee2b63
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,15 @@
+{
+ "extends": "@vue/tsconfig/tsconfig.dom.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": false,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..e879ef1
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": false,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..bbcf80c
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [vue()],
+})