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
8 changes: 8 additions & 0 deletions src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -225,4 +225,12 @@ pre code.hljs {

.mdc-button--outlined:not(:disabled) {
border-color: var(--color-border) !important;
}

.mdc-menu-surface {
background-color: var(--color-background) !important;
}

.material-icons {
color: var(--color-text) !important;
}
17 changes: 17 additions & 0 deletions src/hooks/useMediaQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ref } from 'vue'

/**
* Returns boolean ref, whose value is true if the passed media query
* matches else returns false
* @param mediaQuery string
*/
export default function useMediaQuery(mediaQuery: string) {
const matcher = window.matchMedia(mediaQuery)
const matchMedia = ref<boolean>(matcher.matches)

matcher.onchange = (e) => {
matchMedia.value = e.matches
}

return matchMedia
}
2 changes: 1 addition & 1 deletion src/hooks/useStorage.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const storageHandler = {
load(key: string, autoParse = true) {
load(key: string, autoParse = true): any {
try {
let content = localStorage.getItem(key)
if (content === 'undefined') {
Expand Down
48 changes: 33 additions & 15 deletions src/stores/routes.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,49 @@
import useStorage from '@/hooks/useStorage'
import routes from '@/routes'
import { type Route, type Routes } from '@/types/route'
import { defineStore } from 'pinia'

const storage = useStorage()

export const useRoutes = defineStore('routes', {
state: () => ({
routes
}),
getters: {
getRoute: (state) => (slug: string) => {
return state.routes[slug]
isDeleted:
() =>
(slug: string): boolean => {
const deleted_routes: string[] = storage.load('deletedModules') || []
return deleted_routes.includes(slug)
},
getRoute(state) {
return (slug: string): Route | undefined =>
this.isDeleted(slug) ? undefined : state.routes[slug]
},
getRoutes: (state) => () => {
return state.routes
getRoutes(state) {
return (): Routes =>
Object.keys(state.routes)
.filter((e: string) => !this.isDeleted(e))
.reduce((a, b) => ({ ...a, [b]: routes[b] }), {})
},
search: (state) => (term: string) => {
const query = new RegExp(term, 'gi')
search() {
const self = this
return (term: string) => {
const query = new RegExp(term, 'gi')
const routes = self.getRoutes()

if (term.replace(/\s/g, '') === '') {
return state.routes
}
if (term.replace(/\s/g, '') === '') {
return routes
}

return Object.keys(state.routes)
.filter((e) => {
const route = state.routes[e]
return Object.keys(routes)
.filter((e: string) => {
const route = routes[e]

return route.name.match(query)
})
.reduce((a, b) => ({ ...a, [b]: state.routes[b] }), {})
return route.name.match(query)
})
.reduce((a, b) => ({ ...a, [b]: routes[b] }), {})
}
}
}
})
5 changes: 5 additions & 0 deletions src/types/menuItem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface MenuItem {
index: number
text: string
value: string
}
175 changes: 157 additions & 18 deletions src/views/HomeView.vue
Original file line number Diff line number Diff line change
@@ -1,38 +1,49 @@
<script setup lang="ts">
import SearchInput from "@/components/SearchInputComponent.vue";
import useMediaQuery from "@/hooks/useMediaQuery";
import useStorage from "@/hooks/useStorage";
import useThrottle from "@/hooks/useThrottle";
import { useRoutes } from "@/stores/routes";
import { useSnackbar } from '@/stores/snackbar';
import { type NestedComponentRef } from '@/types/componentRef';
import { type MenuItem } from '@/types/menuItem';
import { onMounted, onUnmounted, ref } from "vue";
import { RouterLink } from "vue-router";
const snackbarStore = useSnackbar();

const snackbarStore = useSnackbar();
const routesState = useRoutes();
const allRoutes = routesState.getRoutes();
const routes = ref(allRoutes);
const isSearchState = ref(false);
const searchTerm = ref("");
const searchInput = ref<NestedComponentRef>();
const storage = useStorage();
const starredModules = ref(new Set(storage.load("starredModules") || []));
const expandedSections = ref([true, true]);
const deletedModules = ref<Set<string>>(new Set(storage.load("deletedModules") || []));
const starredModules = ref<Set<string>>(new Set(storage.load("starredModules") || []));
const expandedSections = ref([true, true, false]);
const showOptions = ref(false);
const optionsMenuPosition = ref<{ top: number, left: number }>({
top: 0,
left: 0
});
const selectedModuleId = ref<string | null>(null);
const smallScreen = useMediaQuery("(max-width: 800px)");

function handleChange(newVal: string) {
searchTerm.value = newVal;
expandedSections.value[1] = true;
routes.value = routesState.search(newVal)
isSearchState.value = true;
}

function reset() {
routes.value = allRoutes;
routes.value = routesState.getRoutes();
isSearchState.value = false;

}

const starModule = (e: Event, moduleId: any) => {
e.preventDefault();
const starModule = (e: Event | null, moduleId: any) => {
if (e) e.preventDefault();
try {
starredModules.value = new Set(starredModules.value);
if (!starredModules.value.has(moduleId)) {
Expand All @@ -51,6 +62,54 @@ const starModule = (e: Event, moduleId: any) => {
}
}

const deleteModule = (e: Event | null, moduleId: any) => {
if (e) e.preventDefault();
try {
deletedModules.value = new Set(deletedModules.value);
if (!deletedModules.value.has(moduleId)) {
deletedModules.value.add(moduleId);
} else {
deletedModules.value.delete(moduleId);
}
storage.save('deletedModules', Array.from(deletedModules.value));
routes.value = routesState.getRoutes();
} catch (e) {
if (e instanceof Error) {
snackbarStore.show(`Failed to delete module! Error: ${e.message}`, 'error')
}
}
}

function openOptions(e: Event, id: string) {
e.preventDefault();
showOptions.value = true;
const { top, left } = (e.target as Element).getBoundingClientRect();
optionsMenuPosition.value = {
top: window.scrollY + top,
left: window.scrollX + left
};
selectedModuleId.value = id;
}

function performAction(menuItem: MenuItem) {
const moduleId = selectedModuleId.value;
switch (menuItem.text) {
case 'Star Module':
case 'Unstar Module':
starModule(null, moduleId)
break;
case 'Remove':
case 'Restore':
deleteModule(null, moduleId)
break;
}
}

function closeOptions() {
selectedModuleId.value = null;
showOptions.value = false;
}

function handleKeyPress(e: KeyboardEvent) {
const key = e.key || e.code || e.which || e.keyCode;
if ((key === 'p' || key === 'P' || key === 'KeyP' || key == 80) && e.ctrlKey) {
Expand Down Expand Up @@ -85,19 +144,20 @@ onUnmounted(() => {
@reset="reset" ref="searchInput" :value="searchTerm" />
<ui-collapse with-icon ripple class="collapse" v-model="expandedSections[0]" v-show="!isSearchState">
<template #toggle>
<div class="heading">Starred Modules ({{ starredModules.size }})</div>
<div class="heading">Starred Modules ({{ Array.from(starredModules).filter(route =>
!deletedModules.has(route)).length }})</div>
</template>
<div class="modules">
<div v-if="starredModules.size === 0" class="noModules">
<div v-if="Array.from(starredModules).filter(route => !deletedModules.has(route)).length === 0" class="noModules">
No Starred Module Found!
</div>
<ui-card class="module" outlined v-for="( route, index ) in Array.from(starredModules) " :key="index"
v-show="routes[route].visible !== false">
<ui-card class="module" outlined
v-for="( route, index ) in Array.from(starredModules).filter(route => !deletedModules.has(route))"
:key="index">
<RouterLink :to="'/' + route" class="link">
<ui-card-content class="content" :title="'Click to open ' + routes[route].name + ' module'">
<ui-icon-button :title="(starredModules.has(route) ? 'Unstar' : 'Star') + ' Module'"
:icon="starredModules.has(route) ? 'favorite' : 'favorite_border'" class="star"
@click="starModule($event, route)"></ui-icon-button>
<ui-icon-button title="More Options" class="options" icon="more_vert"
@click="openOptions($event, route)"></ui-icon-button>
<ui-icon v-if="routes[route].icon" class="icon">{{ routes[route].icon }}</ui-icon>
<img v-if="routes[route].image" :src="routes[route].image" :alt="routes[route].name + '\'s icon'"
class="image" />
Expand All @@ -109,15 +169,15 @@ onUnmounted(() => {
</ui-collapse>
<ui-collapse with-icon ripple class="collapse" v-model="expandedSections[1]">
<template #toggle>
<div class="heading">All Modules</div>
<div class="heading">All Modules ({{ Object.keys(routes).length }})</div>
</template>
<div class="modules">
<ui-card class="module" outlined v-for="( route, index ) in Object.keys(routes) " :key="index"
v-show="routes[route].visible !== false">
<RouterLink :to="'/' + route" class="link">
<ui-card-content class="content" :title="'Click to open ' + routes[route].name + ' module'">
<ui-icon-button title="Star Module" :icon="starredModules.has(route) ? 'favorite' : 'favorite_border'"
class="star" @click="starModule($event, route)"></ui-icon-button>
<ui-icon-button title="Options" class="options" icon="more_vert"
@click="openOptions($event, route)"></ui-icon-button>
<ui-icon v-if="routes[route].icon" class="icon">{{ routes[route].icon }}</ui-icon>
<img v-if="routes[route].image" :src="routes[route].image" :alt="routes[route].name + '\'s icon'"
class="image" />
Expand All @@ -127,6 +187,50 @@ onUnmounted(() => {
</ui-card>
</div>
</ui-collapse>
<ui-collapse with-icon ripple class="collapse" v-model="expandedSections[2]" v-show="!isSearchState">
<template #toggle>
<div class="heading">Deleted Modules ({{ deletedModules.size }})</div>
</template>
<div class="modules">
<div v-if="deletedModules.size === 0" class="noModules">
No Deleted Module Found!
</div>
<ui-card class="module deleted" outlined v-for="( route, index ) in Array.from(deletedModules) " :key="index">
<RouterLink to="#" class="link deleted">
<ui-card-content class="content deleted">
<ui-icon-button title="More Options" class="options" icon="more_vert"
@click="openOptions($event, route)"></ui-icon-button>
<ui-icon v-if="routesState.routes[route].icon" class="icon">{{ routesState.routes[route].icon }}</ui-icon>
<img v-if="routesState.routes[route].image" :src="routesState.routes[route].image"
:alt="routesState.routes[route].name + '\'s icon'" class="image" />
<h3 class="heading">{{ routesState.routes[route].name }}</h3>
</ui-card-content>
</RouterLink>
</ui-card>
</div>
</ui-collapse>
<ui-menu-anchor class="menu-anchor" :style="smallScreen ? {
bottom: 0,
left: '50%'
} : { top: optionsMenuPosition.top + 'px', left: optionsMenuPosition.left + 'px' }">
<ui-menu v-model="showOptions" @selected="performAction" @closed="closeOptions" class="menu"
:aria-role="smallScreen && 'dialog'">
<ui-menuitem v-if="!deletedModules.has(selectedModuleId || '')">
<ui-menuitem-icon>
<ui-icon>{{ starredModules.has(selectedModuleId || '') ? 'favorite' : 'favorite_border' }}</ui-icon>
</ui-menuitem-icon>
<ui-menuitem-text>{{ starredModules.has(selectedModuleId || '') ? 'Unstar Module' : 'Star Module'
}}</ui-menuitem-text>
</ui-menuitem>
<ui-menuitem>
<ui-menuitem-icon>
<ui-icon>{{ deletedModules.has(selectedModuleId || '') ? 'restore_from_trash' : 'delete' }}</ui-icon>
</ui-menuitem-icon>
<ui-menuitem-text>{{ deletedModules.has(selectedModuleId || '') ? 'Restore' : 'Remove' }}</ui-menuitem-text>
</ui-menuitem>
</ui-menu>
<div class="menu-opacity" v-if="smallScreen" v-show="showOptions"></div>
</ui-menu-anchor>
<div v-if="Object.keys(routes).filter(e => routes[e].visible !== false).length === 0" class="noResults">
No Module Found!
</div>
Expand Down Expand Up @@ -162,6 +266,10 @@ main {
margin: 5px;
}

.modules .deleted {
cursor: no-drop !important;
}

.modules .module .content {
width: 100%;
height: 250px;
Expand Down Expand Up @@ -208,14 +316,45 @@ main {
font-size: 24px;
}

.star {
.options {
position: absolute;
top: 10px;
right: 10px;
right: 5px;
}

.menu-anchor {
position: absolute;
}

.collapse {
width: 100%;
margin-top: 5px;
}

@media screen and (max-width: 800px) {
.menu-anchor {
position: fixed;
z-index: 9999;
}

.menu-anchor .menu-opacity {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(var(--color-background-rgb), 0.75);
z-index: 1;
}

.menu-anchor .menu {
position: absolute;
width: 100vw;
max-width: 97.5vw;
left: 0;
transform: translate(-50%);
border-radius: 10px 10px 0 0;
z-index: 2;
}
}
</style>
5 changes: 5 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
{
"include": ["env.d.ts", "global.d.ts", "src/**/*", "src/**/*.vue"],
"files": [],
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.node.json"
Expand Down