Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jasper 176: User Profile Settings/Dark Mode #124

Merged
merged 4 commits into from
Jan 6, 2025
Merged
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
18 changes: 18 additions & 0 deletions web/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"vuetify": "^3.7.5"
},
"devDependencies": {
"@faker-js/faker": "^9.3.0",
"@mdi/font": "^7.4.47",
"@mdi/js": "^7.4.47",
"@typescript-eslint/eslint-plugin": "^8.17.0",
Expand Down
57 changes: 37 additions & 20 deletions web/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
<template>
<v-app>
<v-app-bar app>
<v-app-bar-title>
<router-link to="/"> JASPER </router-link>
</v-app-bar-title>
<v-tabs align-tabs="start">
<v-tab to="/dashboard">Dashboard</v-tab>
<v-tab to="/court-list">Court list</v-tab>
<v-tab to="/court-file-search">Court file search</v-tab>
</v-tabs>
</v-app-bar>
<v-main>
<router-view />
</v-main>
</v-app>
<v-theme-provider :theme="theme">
<v-app>
<profile-off-canvas v-model="profile" @close="profile = false" />
<v-app-bar app>
<v-app-bar-title>
<router-link to="/"> JASPER </router-link>
</v-app-bar-title>
<v-tabs align-tabs="start">
<v-tab to="/dashboard">Dashboard</v-tab>
<v-tab to="/court-list">Court list</v-tab>
<v-tab to="/court-file-search">Court file search</v-tab>
<v-spacer></v-spacer>
<div class="d-flex align-center">
<v-btn
class="ma-2"
@click.stop="profile = true"
:icon="mdiAccountCircle"
size="x-large"
style="font-size: 1.5rem"
/>
</div>
</v-tabs>
</v-app-bar>

<v-main>
<router-view />
</v-main>
</v-app>
</v-theme-provider>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
<script setup lang="ts">
import { mdiAccountCircle } from '@mdi/js';
import { ref } from 'vue';
import ProfileOffCanvas from './components/shared/ProfileOffCanvas.vue';
import { useThemeStore } from './stores/ThemeStore';

export default defineComponent({
name: 'App',
});
const themeStore = useThemeStore();
const theme = ref(themeStore.state);
const profile = ref(false);
</script>

<style>
Expand Down
45 changes: 45 additions & 0 deletions web/src/components/shared/ProfileOffCanvas.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<v-navigation-drawer location="right" temporary>
<v-list-item subtitle="JSmith" color="primary" rounded="shaped">
<template v-slot:prepend>
<v-icon :icon="mdiAccountCircle" size="45" />
</template>
<v-list-item-title>John Smith</v-list-item-title>
<template v-slot:append>
<v-btn :icon="mdiCloseCircle" @click="$emit('close')" />
</template>
</v-list-item>

<v-divider></v-divider>

<v-list-item color="primary" rounded="shaped">
<template v-slot:prepend>
<v-icon :icon="mdiWeatherNight"></v-icon>
</template>
<v-list-item-title>Dark mode</v-list-item-title>
<template v-slot:append>
<v-switch v-model="isDark" hide-details @click="toggleDark" />
</template>
</v-list-item>
</v-navigation-drawer>
</template>

<script setup lang="ts">
import { useThemeStore } from '@/stores/ThemeStore';
import { mdiAccountCircle, mdiCloseCircle, mdiWeatherNight } from '@mdi/js';
import { ref } from 'vue';

const themeStore = useThemeStore();
const theme = ref(themeStore.state);
const isDark = ref(theme.value === 'dark');

function toggleDark() {
themeStore.changeState(theme.value === 'dark' ? 'light' : 'dark');
}
</script>

<style>
div.v-list-item__spacer {
width: 15px !important;
}
</style>
14 changes: 14 additions & 0 deletions web/src/stores/ThemeStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ref } from 'vue';

// We want the default experience to be light mode
const theme = localStorage.getItem('theme') ?? 'light';
const state = ref(theme);

const changeState = (newTheme: string) => {
localStorage.setItem('theme', newTheme);
state.value = newTheme;
};

export const useThemeStore = () => {
return { state, changeState };
};
39 changes: 39 additions & 0 deletions web/tests/components/shared/ProfileOffCanvas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { mount } from '@vue/test-utils';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import ProfileOffCanvas from 'CMP/shared/ProfileOffCanvas.vue';

vi.mock('SRC/stores/ThemeStore', () => ({
useThemeStore: () => ({
theme: 'light',
setTheme: vi.fn(),
changeState: vi.fn(),
}),
}));

describe('ProfileOffCanvas.vue', () => {
let wrapper;

beforeEach(() => {
wrapper = mount(ProfileOffCanvas);
});

it('renders the component', () => {
expect(wrapper.exists()).toBe(true);
});

// Unable to dive deeper into slotted append/prepend components
// todo: find a way to test the slotted components
// it('calls close when close button clicked', async () => {
// await wrapper.find('v-button').trigger('click');

// expect(wrapper.emitted()).toHaveProperty('close');
// });

// it('calls set theme to dark when toggle button is clicked', async () => {
// await wrapper.find('v-switch').trigger('click');

// expect(themeStore.changeState).toHaveBeenCalledWith('dark');
// });
});


30 changes: 30 additions & 0 deletions web/tests/stores/ThemeStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { useThemeStore } from '@/stores/ThemeStore';

describe('ThemeStore', () => {
let themeStore: ReturnType<typeof useThemeStore>;

beforeEach(() => {
themeStore = useThemeStore();
});

afterEach(() => {
localStorage.clear();
});

it('should initialize with light theme by default', () => {
expect(themeStore.state.value).toBe('light');
});

it('should change theme and update localStorage', () => {
themeStore.changeState('dark');
expect(themeStore.state.value).toBe('dark');
expect(localStorage.getItem('theme')).toBe('dark');
});

it('should persist theme from localStorage', () => {
localStorage.setItem('theme', 'dark');
themeStore = useThemeStore();
expect(themeStore.state.value).toBe('dark');
});
});
4 changes: 4 additions & 0 deletions web/vitest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ export default defineConfig({
},
test: {
alias: [
{ find: '@', replacement: resolve(basePath, './src') },
{ find: 'SRC', replacement: resolve(basePath, './src') },
{ find: 'CMP', replacement: resolve(basePath, './src/components') }
],
deps: {
inline: ['vuetify']
},
css: true,
environment: 'happy-dom',
globals: true,
Expand Down
Loading