Initial commit, project structure and domain models

This commit is contained in:
James Wampler
2026-04-07 09:14:06 -07:00
commit 7b15086fe5
111 changed files with 19818 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
presets: [
['@babel/preset-env', {
targets: { node: 'current' },
modules: 'commonjs',
}],
['@babel/preset-typescript', {
allExtensions: true,
isTSX: true,
}],
],
};
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="MicCheck microphone logo">
<title>MicCheck</title>
<!-- Microphone capsule body -->
<rect x="22" y="6" width="20" height="28" rx="10" ry="10"
fill="#FFFFFF" stroke="#BBDEFB" stroke-width="1.5"/>
<!-- Microphone grille lines -->
<line x1="22" y1="16" x2="42" y2="16" stroke="#90CAF9" stroke-width="1" stroke-linecap="round"/>
<line x1="22" y1="22" x2="42" y2="22" stroke="#90CAF9" stroke-width="1" stroke-linecap="round"/>
<line x1="22" y1="28" x2="42" y2="28" stroke="#90CAF9" stroke-width="1" stroke-linecap="round"/>
<!-- Neck / stem going down from capsule -->
<rect x="30" y="34" width="4" height="8" rx="1"
fill="#BBDEFB"/>
<!-- Curved arm (yoke) of the stand -->
<path d="M18 26 Q18 44 32 44 Q46 44 46 26"
fill="none" stroke="#BBDEFB" stroke-width="2.5" stroke-linecap="round"/>
<!-- Vertical stand pole -->
<rect x="30" y="44" width="4" height="12" rx="1"
fill="#BBDEFB"/>
<!-- Horizontal base -->
<rect x="18" y="56" width="28" height="4" rx="2"
fill="#FFFFFF" stroke="#BBDEFB" stroke-width="1.5"/>
<!-- Sound wave arcs (left) -->
<path d="M15 18 Q10 26 15 34" fill="none" stroke="rgba(255,255,255,0.7)"
stroke-width="2" stroke-linecap="round"/>
<path d="M10 14 Q3 26 10 38" fill="none" stroke="rgba(255,255,255,0.4)"
stroke-width="2" stroke-linecap="round"/>
<!-- Sound wave arcs (right) -->
<path d="M49 18 Q54 26 49 34" fill="none" stroke="rgba(255,255,255,0.7)"
stroke-width="2" stroke-linecap="round"/>
<path d="M54 14 Q61 26 54 38" fill="none" stroke="rgba(255,255,255,0.4)"
stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><title>MicCheck Admin</title></head><body><div id="app"></div><script defer="defer" src="/js/929.737c1237.js"></script><script defer="defer" src="/js/main.c5e9ac61.js"></script></body></html>
File diff suppressed because one or more lines are too long
+29
View File
@@ -0,0 +1,29 @@
/*!
* vue-router v4.6.4
* (c) 2025 Eduardo San Martin Morote
* @license MIT
*/
/**
* WCAG 3.0 APCA perceptual contrast algorithm from https://github.com/Myndex/SAPC-APCA
* @licence https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
* @see https://www.w3.org/WAI/GL/task-forces/silver/wiki/Visual_Contrast_of_Text_Subgroup
*/
/**
* @vue/reactivity v3.5.31
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/
/**
* @vue/runtime-dom v3.5.31
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/
/**
* @vue/shared v3.5.31
* (c) 2018-present Yuxi (Evan) You and Vue contributors
* @license MIT
**/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+36
View File
@@ -0,0 +1,36 @@
/** @type {import('jest').Config} */
module.exports = {
testEnvironment: 'jsdom',
testEnvironmentOptions: {
customExportConditions: ['node', 'node-addons'],
},
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: [
'**/__tests__/**/*.spec.[jt]s?(x)',
'**/*.spec.[jt]s?(x)',
'**/*.test.[jt]s?(x)',
],
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'vue', 'json'],
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.[jt]sx?$': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!(vuetify)/)',
],
moduleNameMapper: {
'\\.(css|scss|sass)$': '<rootDir>/tests/__mocks__/styleMock.js',
'\\.(svg|png|jpg|jpeg|gif|webp|woff2?)$': '<rootDir>/tests/__mocks__/fileMock.js',
'^@/(.*)$': '<rootDir>/src/$1',
},
globals: {
'vue-jest': {
tsConfig: './tsconfig.json',
},
},
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
collectCoverageFrom: [
'src/**/*.{ts,vue}',
'!src/main.ts',
],
};
+12940
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -0,0 +1,45 @@
{
"name": "mic-check-admin",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "webpack --mode production --config webpack.config.js",
"build:dev": "webpack --mode development --config webpack.config.js",
"serve": "webpack serve --mode development --config webpack.config.js",
"test": "jest --passWithNoTests"
},
"dependencies": {
"@mdi/font": "^7.4.47",
"vue": "^3.4.0",
"vue-router": "^4.3.0",
"vuetify": "^3.6.0"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.24.0",
"@babel/preset-typescript": "^7.24.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.12.0",
"@vue/test-utils": "^2.4.6",
"@vue/vue3-jest": "^29.2.6",
"babel-jest": "^29.7.0",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.6.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"sass": "^1.77.2",
"sass-loader": "^14.2.1",
"style-loader": "^4.0.0",
"ts-jest": "^29.1.4",
"ts-loader": "^9.5.1",
"typescript": "^5.4.5",
"vue-loader": "^17.4.2",
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4",
"webpack-plugin-vuetify": "^3.1.0"
},
"overrides": {
"brace-expansion": "^5.0.5"
}
}
+12
View File
@@ -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" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<title>MicCheck Admin</title>
</head>
<body>
<div id="app"></div>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
<template>
<AppLayout />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import AppLayout from '@/components/AppLayout.vue';
export default defineComponent({
name: 'App',
components: { AppLayout },
});
</script>
+40
View File
@@ -0,0 +1,40 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="MicCheck microphone logo">
<title>MicCheck</title>
<!-- Microphone capsule body -->
<rect x="22" y="6" width="20" height="28" rx="10" ry="10"
fill="#FFFFFF" stroke="#BBDEFB" stroke-width="1.5"/>
<!-- Microphone grille lines -->
<line x1="22" y1="16" x2="42" y2="16" stroke="#90CAF9" stroke-width="1" stroke-linecap="round"/>
<line x1="22" y1="22" x2="42" y2="22" stroke="#90CAF9" stroke-width="1" stroke-linecap="round"/>
<line x1="22" y1="28" x2="42" y2="28" stroke="#90CAF9" stroke-width="1" stroke-linecap="round"/>
<!-- Neck / stem going down from capsule -->
<rect x="30" y="34" width="4" height="8" rx="1"
fill="#BBDEFB"/>
<!-- Curved arm (yoke) of the stand -->
<path d="M18 26 Q18 44 32 44 Q46 44 46 26"
fill="none" stroke="#BBDEFB" stroke-width="2.5" stroke-linecap="round"/>
<!-- Vertical stand pole -->
<rect x="30" y="44" width="4" height="12" rx="1"
fill="#BBDEFB"/>
<!-- Horizontal base -->
<rect x="18" y="56" width="28" height="4" rx="2"
fill="#FFFFFF" stroke="#BBDEFB" stroke-width="1.5"/>
<!-- Sound wave arcs (left) -->
<path d="M15 18 Q10 26 15 34" fill="none" stroke="rgba(255,255,255,0.7)"
stroke-width="2" stroke-linecap="round"/>
<path d="M10 14 Q3 26 10 38" fill="none" stroke="rgba(255,255,255,0.4)"
stroke-width="2" stroke-linecap="round"/>
<!-- Sound wave arcs (right) -->
<path d="M49 18 Q54 26 49 34" fill="none" stroke="rgba(255,255,255,0.7)"
stroke-width="2" stroke-linecap="round"/>
<path d="M54 14 Q61 26 54 38" fill="none" stroke="rgba(255,255,255,0.4)"
stroke-width="2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+79
View File
@@ -0,0 +1,79 @@
<template>
<v-app-bar
color="primary"
elevation="2"
height="64"
>
<template #prepend>
<v-app-bar-title class="d-flex align-center">
<img
:src="logoUrl"
alt="MicCheck logo"
height="40"
width="40"
class="mr-3"
data-testid="app-logo"
/>
<span class="text-h6 font-weight-bold text-white">MicCheck</span>
</v-app-bar-title>
</template>
<!-- Center: Main navigation links (hidden on xs/sm) -->
<div class="d-none d-md-flex align-center ga-2 mx-auto">
<v-btn
v-for="link in navLinks"
:key="link.route"
:to="link.route"
variant="text"
color="white"
rounded="lg"
:data-testid="`nav-link-${link.label.toLowerCase()}`"
>
{{ link.label }}
</v-btn>
</div>
<template #append>
<div class="d-flex align-center ga-2 mr-2">
<v-btn
to="/profile"
variant="text"
color="white"
prepend-icon="mdi-account-circle"
data-testid="profile-link"
rounded="lg"
>
John Doe
</v-btn>
<v-btn
to="/settings"
icon
variant="text"
color="white"
data-testid="settings-link"
>
<v-icon>mdi-cog-outline</v-icon>
</v-btn>
</div>
</template>
</v-app-bar>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import logoUrl from '@/assets/logo.svg';
export default defineComponent({
name: 'AppHeader',
setup() {
const navLinks = [
{ label: 'Features', route: '/features' },
{ label: 'Environments', route: '/environments' },
{ label: 'Projects', route: '/projects' },
];
return { navLinks, logoUrl };
},
});
</script>
+64
View File
@@ -0,0 +1,64 @@
<template>
<v-app :theme="theme">
<AppHeader />
<v-navigation-drawer
v-model="drawer"
:rail="rail"
permanent
color="white"
>
<v-list density="compact" nav>
<v-list-item
v-for="link in navLinks"
:key="link.route"
:to="link.route"
:prepend-icon="link.icon"
:title="link.label"
active-color="primary"
rounded="lg"
/>
</v-list>
<template #append>
<v-list density="compact" nav>
<v-list-item
:prepend-icon="rail ? 'mdi-chevron-right' : 'mdi-chevron-left'"
title="Collapse"
@click="rail = !rail"
/>
</v-list>
</template>
</v-navigation-drawer>
<v-main>
<v-container fluid class="pa-6">
<router-view />
</v-container>
</v-main>
</v-app>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import AppHeader from '@/components/AppHeader.vue';
export default defineComponent({
name: 'AppLayout',
components: { AppHeader },
setup() {
const drawer = ref(true);
const rail = ref(false);
const theme = ref('light');
const navLinks = [
{ label: 'Dashboard', route: '/', icon: 'mdi-view-dashboard' },
{ label: 'Features', route: '/features', icon: 'mdi-flag-outline' },
{ label: 'Environments', route: '/environments', icon: 'mdi-server-outline' },
{ label: 'Projects', route: '/projects', icon: 'mdi-folder-outline' },
];
return { drawer, rail, theme, navLinks };
},
});
</script>
+10
View File
@@ -0,0 +1,10 @@
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import vuetify from './plugins/vuetify';
import '@mdi/font/css/materialdesignicons.css';
const app = createApp(App);
app.use(router);
app.use(vuetify);
app.mount('#app');
+29
View File
@@ -0,0 +1,29 @@
import 'vuetify/styles';
import { createVuetify } from 'vuetify';
import { aliases, mdi } from 'vuetify/iconsets/mdi';
export default createVuetify({
icons: {
defaultSet: 'mdi',
aliases,
sets: { mdi },
},
theme: {
defaultTheme: 'light',
themes: {
light: {
colors: {
primary: '#1565C0',
secondary: '#546E7A',
accent: '#00ACC1',
error: '#D32F2F',
info: '#0288D1',
success: '#388E3C',
warning: '#F57C00',
background: '#F5F7FA',
surface: '#FFFFFF',
},
},
},
},
});
+51
View File
@@ -0,0 +1,51 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import DashboardView from '@/views/DashboardView.vue';
import FeaturesView from '@/views/FeaturesView.vue';
import EnvironmentsView from '@/views/EnvironmentsView.vue';
import ProjectsView from '@/views/ProjectsView.vue';
import ProfileView from '@/views/ProfileView.vue';
import SettingsView from '@/views/SettingsView.vue';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Dashboard',
component: DashboardView,
},
{
path: '/features',
name: 'Features',
component: FeaturesView,
},
{
path: '/environments',
name: 'Environments',
component: EnvironmentsView,
},
{
path: '/projects',
name: 'Projects',
component: ProjectsView,
},
{
path: '/profile',
name: 'Profile',
component: ProfileView,
},
{
path: '/settings',
name: 'Settings',
component: SettingsView,
},
{
path: '/:pathMatch(.*)*',
redirect: '/',
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
export default router;
+10
View File
@@ -0,0 +1,10 @@
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, unknown>
export default component
}
declare module '*.svg' {
const src: string
export default src
}
+18
View File
@@ -0,0 +1,18 @@
<template>
<v-row>
<v-col cols="12">
<h1 class="text-h4 mb-4">Dashboard</h1>
<v-card>
<v-card-text>
Welcome to MicCheck. Use the navigation to manage your features,
environments, and projects.
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({ name: 'DashboardView' });
</script>
+17
View File
@@ -0,0 +1,17 @@
<template>
<v-row>
<v-col cols="12">
<h1 class="text-h4 mb-4">Environments</h1>
<v-card>
<v-card-text>
Configure deployment environments such as production, staging, and development.
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({ name: 'EnvironmentsView' });
</script>
+17
View File
@@ -0,0 +1,17 @@
<template>
<v-row>
<v-col cols="12">
<h1 class="text-h4 mb-4">Features</h1>
<v-card>
<v-card-text>
Manage feature flags across projects and environments.
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({ name: 'FeaturesView' });
</script>
+17
View File
@@ -0,0 +1,17 @@
<template>
<v-row>
<v-col cols="12">
<h1 class="text-h4 mb-4">Profile</h1>
<v-card>
<v-card-text>
Manage your account details and preferences. Logged in as John Doe.
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({ name: 'ProfileView' });
</script>
+17
View File
@@ -0,0 +1,17 @@
<template>
<v-row>
<v-col cols="12">
<h1 class="text-h4 mb-4">Projects</h1>
<v-card>
<v-card-text>
Organize your applications into projects to manage flags independently.
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({ name: 'ProjectsView' });
</script>
+17
View File
@@ -0,0 +1,17 @@
<template>
<v-row>
<v-col cols="12">
<h1 class="text-h4 mb-4">Settings</h1>
<v-card>
<v-card-text>
Application-wide configuration and administration settings.
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({ name: 'SettingsView' });
</script>
+1
View File
@@ -0,0 +1 @@
module.exports = 'test-file-stub';
+1
View File
@@ -0,0 +1 @@
module.exports = {};
+21
View File
@@ -0,0 +1,21 @@
import { config } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import * as components from 'vuetify/components';
import * as directives from 'vuetify/directives';
// Stub ResizeObserver which jsdom doesn't implement
global.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
// Stub CSS.supports
Object.defineProperty(window, 'CSS', {
value: { supports: () => false },
writable: true,
});
const vuetify = createVuetify({ components, directives });
config.global.plugins = [vuetify];
+68
View File
@@ -0,0 +1,68 @@
import { describe, it, expect, beforeEach } from '@jest/globals';
import { mount, VueWrapper } from '@vue/test-utils';
import { createRouter, createMemoryHistory } from 'vue-router';
import AppHeader from '@/components/AppHeader.vue';
function makeRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: { template: '<div />' } },
{ path: '/features', component: { template: '<div />' } },
{ path: '/environments', component: { template: '<div />' } },
{ path: '/projects', component: { template: '<div />' } },
{ path: '/profile', component: { template: '<div />' } },
{ path: '/settings', component: { template: '<div />' } },
],
});
}
describe('AppHeader', () => {
let wrapper: VueWrapper;
beforeEach(async () => {
const router = makeRouter();
await router.push('/');
// v-app-bar requires a v-app layout context — wrap the component under test
wrapper = mount(
{ template: '<v-app><AppHeader /></v-app>', components: { AppHeader } },
{ global: { plugins: [router] } },
);
});
it('renders the Features navigation link', () => {
const link = wrapper.find('[data-testid="nav-link-features"]');
expect(link.exists()).toBe(true);
expect(link.text()).toBe('Features');
});
it('renders the Environments navigation link', () => {
const link = wrapper.find('[data-testid="nav-link-environments"]');
expect(link.exists()).toBe(true);
expect(link.text()).toBe('Environments');
});
it('renders the Projects navigation link', () => {
const link = wrapper.find('[data-testid="nav-link-projects"]');
expect(link.exists()).toBe(true);
expect(link.text()).toBe('Projects');
});
it('renders the SVG microphone logo image', () => {
const logo = wrapper.find('[data-testid="app-logo"]');
expect(logo.exists()).toBe(true);
expect(logo.attributes('alt')).toBe('MicCheck logo');
});
it('renders the profile link with John Doe text', () => {
const link = wrapper.find('[data-testid="profile-link"]');
expect(link.exists()).toBe(true);
expect(link.text()).toContain('John Doe');
});
it('renders the settings gear icon link', () => {
const link = wrapper.find('[data-testid="settings-link"]');
expect(link.exists()).toBe(true);
});
});
+32
View File
@@ -0,0 +1,32 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"types": ["node", "jest"]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}
+100
View File
@@ -0,0 +1,100 @@
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VuetifyPlugin } = require('webpack-plugin-vuetify');
module.exports = (env, argv) => {
const isProd = argv.mode === 'production';
return {
entry: './src/main.ts',
output: {
filename: isProd ? 'js/[name].[contenthash:8].js' : 'js/[name].js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/',
clean: true,
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/,
options: {
appendTsSuffixTo: [/\.vue$/],
transpileOnly: false,
},
},
{
test: /\.s[ac]ss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.svg$/,
type: 'asset/resource',
generator: {
filename: 'img/[name].[hash:8][ext]',
},
},
{
test: /\.(png|jpe?g|gif|webp)$/,
type: 'asset/resource',
generator: {
filename: 'img/[name].[hash:8][ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]',
},
},
],
},
plugins: [
new VueLoaderPlugin(),
new VuetifyPlugin({ autoImport: true }),
new HtmlWebpackPlugin({
template: './public/index.html',
filename: 'index.html',
inject: 'body',
}),
],
devServer: {
port: 8080,
hot: true,
historyApiFallback: true,
static: {
directory: path.resolve(__dirname, 'public'),
},
},
devtool: isProd ? 'source-map' : 'eval-cheap-module-source-map',
optimization: {
splitChunks: {
chunks: 'all',
},
},
};
};