Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions devNotes.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 13 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CMS</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
33 changes: 23 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
13 changes: 13 additions & 0 deletions public/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<header class="app-title">Client Management System</header>
<RouterView />
</template>

<style scoped lang="scss">
@use '/src/styles/variables' as vars;

.app-title {
font-size: 2rem;
font-weight: bold;
text-align: center;
background-color: vars.$background-color;
padding: 2rem 0 1rem;
margin: 0;
color: vars.$text-color;
user-select: none;
}
</style>
52 changes: 52 additions & 0 deletions src/apiService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { RecordItem } from "./record";

const API_URL = 'http://localhost:3000';

async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
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<RecordItem[]> {
return request<RecordItem[]>('/clients');
}

export async function getRecordedById(id: number): Promise<RecordItem> {
return request<RecordItem>(`/clients/${id}`);
}

export async function createRecord(record: any): Promise<RecordItem> {
return request<RecordItem>(`/clients`, {
method: 'POST',
body: JSON.stringify(record)
});
}

export async function updateRecord(id: number, record: any): Promise<RecordItem> {
return request<RecordItem>(`/clients/${id}`, {
method: 'PUT',
body: JSON.stringify(record)
});
}

export async function deleteRecord(id: number): Promise<RecordItem> {
return request<RecordItem>(`/clients/${id}`, {
method: 'DELETE'
});
}

export default {
getRecords,
getRecordedById,
createRecord,
updateRecord,
deleteRecord
};
Loading