add time overview page

This commit is contained in:
Gregor Vostrak
2024-03-26 18:19:08 +01:00
parent ab9a1d2fab
commit 26fef8b9f7
52 changed files with 2463 additions and 972 deletions

View File

@@ -1,51 +0,0 @@
<script setup lang="ts">
import { twMerge } from 'tailwind-merge';
const props = withDefaults(
defineProps<{
name: string;
size: 'base' | 'large';
tag: string;
class?: string;
color: string;
}>(),
{
size: 'base',
tag: 'div',
color: 'var(--theme-color-icon-default)',
}
);
const indicatorClasses = {
base: 'w-2.5 h-2.5',
large: 'w-3 h-3',
};
const badgeClasses = {
base: 'py-1 px-2 space-x-1.5 text-xs',
large: 'py-1.5 px-3 space-x-2 text-sm text-muted',
};
</script>
<template>
<component
:is="tag"
:class="
twMerge(
props.class,
badgeClasses[size],
'border-input-border border rounded inline-flex items-center font-semibold text-white'
)
">
<div
:style="{ backgroundColor: color }"
:class="
twMerge(indicatorClasses[size], 'inline-block rounded-full')
"></div>
<span>
{{ name }}
</span>
</component>
</template>
<style scoped></style>

View File

@@ -1,163 +0,0 @@
<script setup lang="ts">
import ProjectBadge from '@/Components/common/ProjectBadge.vue';
import { computed, nextTick, ref, watch } from 'vue';
import { type Project, useProjectsStore } from '@/utils/useProjects';
import Dropdown from '@/Components/Dropdown.vue';
import {
ComboboxAnchor,
ComboboxContent,
ComboboxInput,
ComboboxItem,
ComboboxRoot,
ComboboxViewport,
} from 'radix-vue';
import { PlusCircleIcon } from '@heroicons/vue/20/solid';
import ProjectDropdownItem from '@/Components/common/ProjectDropdownItem.vue';
import { storeToRefs } from 'pinia';
import { api } from '../../../../openapi.json.client';
import { usePage } from '@inertiajs/vue3';
import { getRandomColor } from '@/utils/color';
const searchValue = ref('');
const searchInput = ref<HTMLElement | null>(null);
const model = defineModel<Project>({
default: undefined,
});
const open = ref(false);
const projectsStore = useProjectsStore();
const { projects } = storeToRefs(projectsStore);
const projectDropdownTrigger = ref<HTMLElement | null>(null);
const shownProjects = computed(() => {
return projects.value.filter((project) => {
return project.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '');
});
});
const page = usePage<{
auth: {
user: {
current_team_id: string;
};
};
}>();
async function addProjectIfNoneExists() {
if (searchValue.value.length > 0 && shownProjects.value.length === 0) {
const response = await api.createProject(
{
name: searchValue.value,
color: getRandomColor(),
},
{ params: { organization: page.props.auth.user.current_team_id } }
);
projects.value.unshift(response.data);
model.value = response.data;
searchValue.value = '';
open.value = false;
}
}
watch(open, (isOpen) => {
if (isOpen) {
nextTick(() => {
// @ts-expect-error We need to access the actual HTML Element to focus as radix-vue does not support any other way right now
searchInput.value?.$el?.focus();
});
projects.value.sort((a) => {
return model.value === a ? -1 : 1;
});
}
});
function isProjectSelected(project: Project) {
return model.value?.id === project.id;
}
const selectedProjectName = computed(() => {
return model.value?.name || 'No Project';
});
const selectedProjectColor = computed(() => {
return model.value?.color || 'var(--theme-color-icon-default)';
});
</script>
<template>
<Dropdown v-model="open" align="right" width="60">
<template #trigger>
<ProjectBadge
ref="projectDropdownTrigger"
:color="selectedProjectColor"
size="large"
tag="button"
:name="selectedProjectName"
class="focus:border-input-border-active focus:outline-0 focus:bg-card-background-seperator hover:bg-card-background-seperator"></ProjectBadge>
</template>
<template #content>
<ComboboxRoot
:open="open"
v-model="model"
v-model:searchTerm="searchValue"
class="relative">
<ComboboxAnchor>
<ComboboxInput
@keydown.enter="addProjectIfNoneExists"
ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-seperator focus:border-card-background-seperator w-full"
placeholder="Search for a project..." />
</ComboboxAnchor>
<ComboboxContent>
<ComboboxViewport ref="dropdownViewport" class="w-60">
<ComboboxItem
v-if="searchValue === ''"
class="data-[highlighted]:bg-card-background-active"
:data-project-id="null"
:value="{
id: null,
name: '',
}">
<ProjectDropdownItem
name="No Project"
color="var(--theme-color-icon-default)"
selected></ProjectDropdownItem>
</ComboboxItem>
<ComboboxItem
v-for="project in shownProjects"
:key="project.id"
:value="project"
class="data-[highlighted]:bg-card-background-active"
:data-project-id="project.id">
<ProjectDropdownItem
:selected="isProjectSelected(project)"
:color="project.color"
:name="project.name"></ProjectDropdownItem>
</ComboboxItem>
<div
v-if="
searchValue.length > 0 &&
shownProjects.length === 0
"
class="bg-card-background-active">
<div
class="flex space-x-3 items-center px-4 py-3 text-xs font-medium border-t rounded-b-lg border-card-background-seperator">
<PlusCircleIcon
class="w-5 flex-shrink-0"></PlusCircleIcon>
<span
>Add "{{ searchValue }}" as a new
Project</span
>
</div>
</div>
</ComboboxViewport>
</ComboboxContent>
</ComboboxRoot>
</template>
</Dropdown>
</template>
<style scoped></style>

View File

@@ -1,19 +0,0 @@
<script setup lang="ts">
defineProps<{
name: string;
selected: boolean;
color: string;
}>();
</script>
<template>
<div
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<div
:style="{ backgroundColor: color }"
class="w-3 h-3 rounded-full"></div>
<span>{{ name }}</span>
</div>
</template>
<style scoped></style>

View File

@@ -1,220 +0,0 @@
<script setup lang="ts">
import { PlusCircleIcon, TagIcon } from '@heroicons/vue/20/solid';
import Dropdown from '@/Components/Dropdown.vue';
import {
type Component,
computed,
nextTick,
onMounted,
ref,
watch,
watchEffect,
} from 'vue';
import TagDropdownItem from '@/Components/common/TagDropdownItem.vue';
import { twMerge } from 'tailwind-merge';
import {
ComboboxAnchor,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxRoot,
ComboboxViewport,
} from 'radix-vue';
import { useTagsStore } from '@/utils/useTags';
import { storeToRefs } from 'pinia';
const tagsStore = useTagsStore();
const { tags } = storeToRefs(tagsStore);
const emit = defineEmits(['changed']);
const model = defineModel<string[]>({
default: [],
});
onMounted(async () => {
await tagsStore.fetchTags();
});
const searchInput = ref<Component | null>(null);
const open = ref(false);
const dropdownViewport = ref<Component | null>(null);
const searchValue = ref('');
function isTagSelected(id: string) {
return model.value.includes(id);
}
function addOrRemoveTagFromSelection(id: string) {
if (model.value.includes(id)) {
model.value = model.value.filter((tagId) => tagId !== id);
} else {
model.value.push(id);
}
emit('changed');
}
const iconColorClasses = computed(() => {
if (model.value.length > 0) {
return 'text-accent-200/80 focus:text-accent-200 hover:text-accent-200';
} else {
return 'text-icon-default hover:text-icon-active focus:text-icon-active';
}
});
watch(open, (isOpen) => {
if (isOpen) {
nextTick(() => {
// @ts-expect-error We need to access the actual HTML Element to focus as radix-vue does not support any other way right now
searchInput.value?.$el?.focus();
});
tags.value.sort((a) => {
return model.value.includes(a.id) ? -1 : 1;
});
}
});
const filteredTags = computed(() => {
return tags.value.filter((tag) => {
return tag.name
.toLowerCase()
.includes(searchValue.value?.toLowerCase()?.trim() || '');
});
});
const showAllTags = ref(false);
const shownTags = computed(() => {
if (showAllTags.value) {
return filteredTags.value;
} else {
return filteredTags.value.slice(0, 5);
}
});
const moreTagsAvailable = computed(() => {
return filteredTags.value.length - shownTags.value.length;
});
async function addTagIfNoneExists() {
if (searchValue.value.length > 0 && filteredTags.value.length === 0) {
const newTag = await tagsStore.createTag(searchValue.value);
addOrRemoveTagFromSelection(newTag.id);
searchValue.value = '';
}
}
function removeTagLimit() {
showAllTags.value = true;
}
watchEffect(() => {
if (searchValue.value === ' ') {
nextTick(() => {
searchValue.value = '';
const currentSelectedItem =
// @ts-expect-error We need to access the actual HTML Element to focus as radix-vue does not support any other way right now
dropdownViewport.value?.$el?.querySelector(
'[data-highlighted]'
);
const highlightedTagId = currentSelectedItem?.getAttribute(
'data-tag-id'
) as string;
if (highlightedTagId) {
const highlightedTag = tags.value.find(
(tag) => tag.id === highlightedTagId
);
if (highlightedTag) {
addOrRemoveTagFromSelection(highlightedTag.id);
}
}
});
}
});
function updateValue(e: string[]) {
model.value = e;
emit('changed');
}
</script>
<template>
<Dropdown width="120" v-model="open" :closeOnContentClick="false">
<template #trigger>
<button
data-testid="tag_dropdown"
:class="
twMerge(
iconColorClasses,
'flex-shrink-0 ring-0 focus:outline-none focus:ring-0 transition focus:bg-card-background-seperator hover:bg-card-background-seperator rounded-full w-11 h-11 flex items-center justify-center'
)
">
<TagIcon class="w-7 h-7"></TagIcon>
<div
v-if="model.length > 1"
class="font-extrabold absolute rounded-full text-xs w-3 h-3 block top-[15px] rotate-[45deg] right-[14px] text-card-background">
{{ model.length }}
</div>
</button>
</template>
<template #content>
<ComboboxRoot
multiple
:open="open"
@update:modelValue="updateValue as any"
v-model:searchTerm="searchValue"
class="relative">
<ComboboxAnchor>
<ComboboxInput
@keydown.enter="addTagIfNoneExists"
data-testid="tag_dropdown_search"
ref="searchInput"
class="bg-card-background border-0 placeholder-muted text-sm text-white py-2.5 focus:ring-0 border-b border-card-background-seperator focus:border-card-background-seperator w-full"
placeholder="Search for a tag..." />
</ComboboxAnchor>
<ComboboxContent>
<ComboboxViewport ref="dropdownViewport" class="w-60">
<ComboboxEmpty>
<div
v-if="searchValue.length > 0"
class="bg-card-background-active">
<div
class="flex space-x-3 items-center px-4 py-3 text-xs font-medium border-t rounded-b-lg border-card-background-seperator">
<PlusCircleIcon
class="w-5 flex-shrink-0"></PlusCircleIcon>
<span
>Add "{{ searchValue }}" as a new
Tag</span
>
</div>
</div>
<div v-else></div>
</ComboboxEmpty>
<ComboboxItem
v-for="tag in shownTags"
:key="tag.id"
:value="tag.id"
class="data-[highlighted]:bg-card-background-active"
data-testid="tag_dropdown_entries"
:data-tag-id="tag.id">
<TagDropdownItem
:selected="isTagSelected(tag.id)"
:name="tag.name"></TagDropdownItem>
</ComboboxItem>
</ComboboxViewport>
<button
@click="removeTagLimit"
v-if="moreTagsAvailable > 0"
class="border-t hover:text-white hover:bg-card-background-active px-2 text-center font-semibold py-2 border-t-card-background-seperator">
Show all
</button>
</ComboboxContent>
</ComboboxRoot>
</template>
</Dropdown>
</template>
<style scoped></style>

View File

@@ -1,28 +0,0 @@
<script setup lang="ts">
import { CheckCircleIcon } from '@heroicons/vue/20/solid';
import { computed } from 'vue';
import { twMerge } from 'tailwind-merge';
const props = defineProps<{
name: string;
selected: boolean;
}>();
const iconClasses = computed(() => {
if (props.selected) {
return 'text-accent-200';
} else {
return 'text-card-border';
}
});
</script>
<template>
<div
class="flex items-center space-x-3 w-full px-3 py-2.5 text-start text-sm font-medium leading-5 text-white hover:bg-card-background-active focus:outline-none focus:bg-card-background-active transition duration-150 ease-in-out">
<CheckCircleIcon :class="twMerge(iconClasses, 'w-5')"></CheckCircleIcon>
<span>{{ name }}</span>
</div>
</template>
<style scoped></style>