Translate projects page

This commit is contained in:
gregortokarev 2024-07-29 09:05:28 +03:00
parent 1a5c36b2d3
commit 148368dce1
4 changed files with 281 additions and 280 deletions

View File

@ -3,59 +3,69 @@ import { useProjectStatusModalStore } from "@store/select-modal.ts";
import { useProjectStatusStore } from "@store/project-status.ts";
import { computed, ref } from "vue";
import SelectModal from "./SelectModal.vue";
import { useI18n } from "vue-i18n";
const projectStatusModalStore = useProjectStatusModalStore();
const projectStatusStore = useProjectStatusStore();
const query = ref("");
const { t } = useI18n({
messages: {
en: {
'not started': "not started",
"in progress": "in progress",
'finished': "finished"
},
ru: {
'not started': "Не начат",
"in progress": "В прогрессе",
'finished': "Завершен"
}
}
})
const projectOptions = computed(() => {
const searchIndex = projectStatusStore.getIndex;
const searchResults = searchIndex.search(query.value);
const searchIndex = projectStatusStore.getIndex;
const searchResults = searchIndex.search(query.value);
const result = query.value
? searchResults.map((r) => r.item)
: projectStatusStore.projectStatuses;
const result = query.value
? searchResults.map((r) => r.item)
: projectStatusStore.projectStatuses;
return result.map((project) => ({
id: project.id,
text: project.title,
icon: project.icon,
}));
return result.map((project) => ({
id: project.id,
text: t(project.title),
icon: project.icon,
}));
});
function onSubmit(id: string) {
projectStatusModalStore.resolveFn && projectStatusModalStore.resolveFn(id);
projectStatusModalStore.resolveFn && projectStatusModalStore.resolveFn(id);
}
const open = computed({
get() {
return projectStatusModalStore.open;
},
set(value: boolean) {
if (!value) {
projectStatusModalStore.close();
query.value = "";
}
},
get() {
return projectStatusModalStore.open;
},
set(value: boolean) {
if (!value) {
projectStatusModalStore.close();
query.value = "";
}
},
});
const checkedIndex = computed(() => {
return projectOptions.value.findIndex(
(p) => p.id === projectStatusModalStore.modalOptions?.statusId,
);
return projectOptions.value.findIndex(
(p) => p.id === projectStatusModalStore.modalOptions?.statusId,
);
});
</script>
<template>
<SelectModal
v-if="projectStatusModalStore.modalOptions"
:hint-text="projectStatusModalStore.modalOptions.hintText"
:checked-index="checkedIndex"
v-model:query="query"
:options="projectOptions"
v-model:open="open"
@submit="onSubmit"
/>
<SelectModal v-if="projectStatusModalStore.modalOptions" :hint-text="projectStatusModalStore.modalOptions.hintText"
:checked-index="checkedIndex" v-model:query="query" :options="projectOptions" v-model:open="open"
@submit="onSubmit" />
</template>
@store/select-modal

View File

@ -6,21 +6,39 @@ import { onClickOutside, useDebounceFn } from "@vueuse/core";
import Checkbox from "@components/UI/Checkbox.vue";
import { useProjectStatusStore } from "@store/project-status.ts";
import Icon from "@components/Icon.vue";
import { useI18n } from "vue-i18n";
const projectStore = useProjectStore();
const projectStatusStore = useProjectStatusStore();
const attrs = useAttrs();
const { t } = useI18n({
messages: {
en: {
'not started': "not started",
"in progress": "in progress",
'finished': "finished"
},
ru: {
'not started': "Не начат",
"in progress": "В прогрессе",
'finished': "Завершен"
}
}
})
const props = defineProps<{
project: Project;
selected: boolean;
project: Project;
selected: boolean;
}>();
const emit = defineEmits<{
(e: "update", value: Partial<Project>): void;
(e: "update:selected", value: boolean): void;
(e: "setStatus", value: void): void;
(e: "update", value: Partial<Project>): void;
(e: "update:selected", value: boolean): void;
(e: "setStatus", value: void): void;
}>();
const showColorPicker = ref(false);
@ -28,103 +46,78 @@ const colorPicker = ref<HTMLElement | null>(null);
const color = ref<HTMLElement | null>(null);
onClickOutside(colorPicker, () => {
showColorPicker.value = false;
showColorPicker.value = false;
});
const colorBound = computed(() => {
if (!color.value) return;
if (!color.value) return;
return color.value.getBoundingClientRect();
return color.value.getBoundingClientRect();
});
const projectStatus = computed<ProjectStatus | undefined>(() => {
if (!props.project.statusId) return;
if (!props.project.statusId) return;
return projectStatusStore.projectStatuses.find(
(ps) => ps.id === props.project.statusId,
);
return projectStatusStore.projectStatuses.find(
(ps) => ps.id === props.project.statusId,
);
});
function editDraft(event: Event) {
const target = event.target as HTMLElement;
const value = target.textContent;
const target = event.target as HTMLElement;
const value = target.textContent;
value && emit("update", { title: value });
value && emit("update", { title: value });
}
const onEditDraft = useDebounceFn(editDraft, 1000);
function onPickColor(color: string) {
emit("update", { color });
emit("update", { color });
showColorPicker.value = false;
showColorPicker.value = false;
}
function onEnter(event: Event) {
const target = event.currentTarget as HTMLElement;
target.blur();
const target = event.currentTarget as HTMLElement;
target.blur();
}
</script>
<template>
<div
class="flex cursor-pointer items-center border-b border-gray-400 py-5 transition-colors hover:bg-gray-50"
:class="{ '!border-black bg-gray-50': selected }"
v-bind="attrs"
>
<Checkbox
class="mr-2"
:model-value="selected"
@update:model-value="emit('update:selected', $event)"
></Checkbox>
<div class="flex w-[56%] items-center space-x-2">
<div
ref="color"
@click="showColorPicker = !showColorPicker"
class="hover:shadow-colorPick h-5 w-5 cursor-pointer rounded-full"
:class="{ [`bg-${project.color}-400`]: true }"
></div>
<span>
#<span
class="outline-0"
contenteditable="true"
@input="onEditDraft"
@keydown.enter.prevent="onEnter"
>
{{ project.title }}
</span>
</span>
<div class="flex cursor-pointer items-center border-b border-gray-400 py-5 transition-colors hover:bg-gray-50"
:class="{ '!border-black bg-gray-50': selected }" v-bind="attrs">
<Checkbox class="mr-2" :model-value="selected" @update:model-value="emit('update:selected', $event)"></Checkbox>
<div class="flex w-[56%] items-center space-x-2">
<div ref="color" @click="showColorPicker = !showColorPicker"
class="hover:shadow-colorPick h-5 w-5 cursor-pointer rounded-full"
:class="{ [`bg-${project.color}-400`]: true }"></div>
<span>
#<span class="outline-0" contenteditable="true" @input="onEditDraft" @keydown.enter.prevent="onEnter">
{{ project.title }}
</span>
</span>
</div>
<div class="flex cursor-pointer items-center space-x-1.5 rounded px-2 py-1 text-xs capitalize text-gray-800 transition-colors hover:bg-gray-100"
@click="emit('setStatus')">
<template v-if="projectStatus">
<Icon :name="projectStatus?.icon" :size="14"></Icon>
<span>{{ t(projectStatus?.title) }}</span>
</template>
<template v-else>
<span>Set status</span>
</template>
</div>
</div>
<div
class="flex cursor-pointer items-center space-x-1.5 rounded px-2 py-1 text-xs capitalize text-gray-800 transition-colors hover:bg-gray-100"
@click="emit('setStatus')"
>
<template v-if="projectStatus">
<Icon :name="projectStatus?.icon" :size="14"></Icon>
<span>{{ projectStatus?.title }}</span>
</template>
<template v-else>
<span>Set status</span>
</template>
</div>
</div>
<teleport to="body">
<div
v-if="showColorPicker && colorBound"
ref="colorPicker"
class="fixed flex w-[156px] -translate-x-1/2 flex-wrap gap-2.5 rounded-lg bg-white p-2 shadow"
:style="{
top: colorBound.top + colorBound.height + 4 + 'px',
left: colorBound.left + 'px',
}"
>
<div
v-for="c in projectStore.colors"
:key="c"
class="hover:shadow-colorPick h-5 w-5 cursor-pointer rounded-full border"
:class="{ [`bg-${c}-400`]: true, 'border-black': c === project.color }"
@click="onPickColor(c)"
></div>
</div>
</teleport>
<teleport to="body">
<div v-if="showColorPicker && colorBound" ref="colorPicker"
class="fixed flex w-[156px] -translate-x-1/2 flex-wrap gap-2.5 rounded-lg bg-white p-2 shadow" :style="{
top: colorBound.top + colorBound.height + 4 + 'px',
left: colorBound.left + 'px',
}">
<div v-for="c in projectStore.colors" :key="c"
class="hover:shadow-colorPick h-5 w-5 cursor-pointer rounded-full border"
:class="{ [`bg-${c}-400`]: true, 'border-black': c === project.color }" @click="onPickColor(c)"></div>
</div>
</teleport>
</template>

View File

@ -111,27 +111,18 @@ app
})
.use(router);
inject() // injecting analytics
function customRule(choice: number, choicesLength: number) {
if (choice === 0) {
return 0
}
function customRule(choice: number, _choicesLength: number) {
const calcValue = Math.abs(choice) % 100;
const num = calcValue % 10;
const teen = choice > 10 && choice < 20
const endsWithOne = choice % 10 === 1
if (!teen && endsWithOne) {
return 1
}
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
return 2
}
return choicesLength < 4 ? 2 : 3
if (calcValue > 10 && calcValue < 20) return 3;
if (num > 1 && num < 5) return 2;
if (num === 1) return 1;
return 3;
}
const i18n = createI18n({
locale: "ru",
fallbackLocale: "en",

View File

@ -11,136 +11,168 @@ import hotkeys from "hotkeys-js";
import { useDeleteModalStore } from "@store/delete-modal";
import { nanoid } from "nanoid";
import { onMounted, onUnmounted, ref } from "vue";
import { useI18n } from "vue-i18n";
const projectStore = useProjectStore();
const projectStatusModalStore = useProjectStatusModalStore();
const { t } = useI18n({
messages: {
en: {
title: "Projects",
create: "New project",
updateStatus: "Update {length} projects?",
column: {
title: "Title",
status: "Status"
},
deleteModal: {
multipleDelete: `Are you sure you want to delete {length} projects?`,
warning: "Delete would be permanent, you cant restore issues later.",
spesificDelete: `Are you sure you want to delete “{title}”?`
},
},
ru: {
title: "Проекты",
create: "Новый проект",
updateStatus: "Обновить {length} проект? | Обновить {length} проекта? | Обновить {length} проектов?",
column: {
title: "Название",
status: "Статус"
},
deleteModal: {
multipleDelete: `Вы уверенны, что хотите удалить {length} проект? | Вы уверенны, что хотите удалить {length} проекта? | Вы уверенны, что хотите удалить {length} проектов?`,
warning: "Удаление будет пермонентным, вы не сможете восстановить проекты",
spesificDelete: `Вы уверены, что хотети удалить “{title}”?`
},
}
}
})
const rowsContainer = ref<HTMLElement | null>(null);
onMounted(async () => {
await projectStore.loadProjects();
await projectStore.loadProjects();
hotkeys("C", onCreateProject);
hotkeys("C", onCreateProject);
hotkeys("x", () => {
if (hoveredProjectId.value) {
updateSelection(hoveredProjectId.value);
}
});
hotkeys("x", () => {
if (hoveredProjectId.value) {
updateSelection(hoveredProjectId.value);
}
});
hotkeys("cmd+backspace", () => {
removeProject();
});
hotkeys("cmd+backspace", () => {
removeProject();
});
hotkeys("S", () => {
onSetStatus();
});
hotkeys("S", () => {
onSetStatus();
});
hotkeys("esc", () => {
selectedProjectsIds.value = [];
});
hotkeys("esc", () => {
selectedProjectsIds.value = [];
});
});
onUnmounted(() => {
hotkeys.unbind("C", "all", onCreateProject);
hotkeys.unbind("C", "all", onCreateProject);
});
function onUpdateProject(id: string, project: Partial<Project>) {
if (project?.title === "") return;
if (project?.title === "") return;
projectStore.edit(id, project);
projectStore.edit(id, project);
}
function onCreateProject() {
projectStore.create("");
projectStore.create("");
setTimeout(() => {
if (!rowsContainer.value) return;
setTimeout(() => {
if (!rowsContainer.value) return;
const addedElement = Array.from(rowsContainer.value.children).find((el) => {
const editable = el.querySelector("[contenteditable]")!;
return editable.textContent === "";
const addedElement = Array.from(rowsContainer.value.children).find((el) => {
const editable = el.querySelector("[contenteditable]")!;
return editable.textContent === "";
});
addedElement?.scrollIntoView();
addedElement &&
focusOnEditableElement(
addedElement.querySelector("[contenteditable]") as HTMLElement,
);
});
addedElement?.scrollIntoView();
addedElement &&
focusOnEditableElement(
addedElement.querySelector("[contenteditable]") as HTMLElement,
);
});
}
const selectedProjectsIds = ref<string[]>([]);
function updateSelection(projectId: string) {
if (selectedProjectsIds.value.includes(projectId)) {
const idx = selectedProjectsIds.value.findIndex((id) => id === projectId);
selectedProjectsIds.value.splice(idx, 1);
return;
}
if (selectedProjectsIds.value.includes(projectId)) {
const idx = selectedProjectsIds.value.findIndex((id) => id === projectId);
selectedProjectsIds.value.splice(idx, 1);
return;
}
selectedProjectsIds.value.push(projectId);
selectedProjectsIds.value.push(projectId);
}
async function onSetStatus() {
if (selectedProjectsIds.value.length <= 0) {
if (!hoveredProjectId.value) return;
if (selectedProjectsIds.value.length <= 0) {
if (!hoveredProjectId.value) return;
const project = projectStore.getOne(hoveredProjectId.value);
if (!project) return;
const project = projectStore.getOne(hoveredProjectId.value);
if (!project) return;
selectedProjectsIds.value = [];
selectedProjectsIds.value = [];
const statusId = await projectStatusModalStore.use({
hintText: project.title,
statusId: project.statusId,
});
projectStore.edit(project.id, { statusId });
return;
}
const statusId = await projectStatusModalStore.use({
hintText: project.title,
statusId: project.statusId,
hintText: t("updateStatus", { length: selectedProjectsIds.value.length }, selectedProjectsIds.value.length),
statusId: null,
});
projectStore.edit(project.id, { statusId });
selectedProjectsIds.value.forEach((id) => {
projectStore.edit(id, { statusId });
});
return;
}
const statusId = await projectStatusModalStore.use({
hintText: `Update ${selectedProjectsIds.value.length} projects`,
statusId: null,
});
selectedProjectsIds.value.forEach((id) => {
projectStore.edit(id, { statusId });
});
selectedProjectsIds.value = [];
selectedProjectsIds.value = [];
}
const deleteModalStore = useDeleteModalStore();
async function removeProject() {
if (selectedProjectsIds.value.length) {
if (selectedProjectsIds.value.length) {
const res = await deleteModalStore.use({
titleText: t("deleteModal.multipleDelete", { length: selectedProjectsIds.value.length }, selectedProjectsIds.value.length),
descriptionText: t("deleteModal.warning"),
});
if (!res) return;
projectStore.remove(selectedProjectsIds.value);
selectedProjectsIds.value = [];
return;
}
const draft = hoveredProjectId.value
? projectStore.getOne(hoveredProjectId.value)
: null;
if (!draft) return;
const res = await deleteModalStore.use({
titleText: `Are you sure you want to delete ${selectedProjectsIds.value.length} drafts?`,
descriptionText:
"Delete would be permanent, you cant restore projects later.",
titleText: t("deleteModal.spesificDelete", { title: draft.title }),
descriptionText: t("deleteModal.warning"),
});
if (!res) return;
projectStore.remove(selectedProjectsIds.value);
selectedProjectsIds.value = [];
return;
}
const draft = hoveredProjectId.value
? projectStore.getOne(hoveredProjectId.value)
: null;
if (!draft) return;
const res = await deleteModalStore.use({
titleText: `Are you sure you want to delete “${draft.title}”?`,
descriptionText:
"Delete would be permanent, you cant restore project later.",
});
res && projectStore.remove(draft.id);
res && projectStore.remove(draft.id);
}
const contextMenuOpen = ref(false);
@ -151,88 +183,63 @@ const clickY = ref(0);
const clickX = ref(0);
function onOpenContextMenu(evt: MouseEvent) {
clickX.value = evt.clientX;
clickY.value = evt.clientY;
clickX.value = evt.clientX;
clickY.value = evt.clientY;
contextMenuOpen.value = true;
setScrolling(false);
contextMenuOpen.value = true;
setScrolling(false);
}
function onListLeave() {
if (!contextMenuOpen.value) {
hoveredProjectId.value = null;
}
if (!contextMenuOpen.value) {
hoveredProjectId.value = null;
}
}
function onSelectOption(action: string) {
if (action === "stat") {
onSetStatus();
} else if (action === "del") {
removeProject();
}
if (action === "stat") {
onSetStatus();
} else if (action === "del") {
removeProject();
}
contextMenuOpen.value = false;
contextMenuOpen.value = false;
}
</script>
<template>
<div class="pt-8">
<div class="mb-6 flex items-center space-x-5">
<h1 class="text-xl">Projects</h1>
<div
v-hint="'C'"
@click="onCreateProject()"
class="cursor-pointer rounded-lg px-1.5 py-[3px] text-gray-600 transition-colors hover:bg-gray-400"
>
New project +
</div>
<div class="pt-8">
<div class="mb-6 flex items-center space-x-5">
<h1 class="text-xl">{{ t("title") }}</h1>
<div v-hint="'C'" @click="onCreateProject()"
class="cursor-pointer rounded-lg px-1.5 py-[3px] text-gray-600 transition-colors hover:bg-gray-400">
{{ t("create") }} +
</div>
</div>
<div v-if="projectStore.projects.length" class="flex items-center space-x-0.5">
<div class="flex w-[59%] items-center pl-[2.4%] text-xs text-gray-600">
<span> {{ t("column.title") }} </span>
</div>
<div class="flex items-center text-xs text-gray-600">
<span> {{ t("column.status") }} </span>
</div>
</div>
<div ref="rowsContainer" class="" @contextmenu.prevent="onOpenContextMenu" @mouseleave="onListLeave">
<ProjectRow v-for="row in projectStore.projects" :project="row" :key="row.id"
:selected="selectedProjectsIds.includes(row.id)" @mouseenter="hoveredProjectId = row.id"
@update:selected="updateSelection(row.id)" @update="onUpdateProject(row.id, $event)"
@setStatus="onSetStatus()" @contextmenu="onOpenContextMenu"></ProjectRow>
</div>
</div>
<div
v-if="projectStore.projects.length"
class="flex items-center space-x-0.5"
>
<div class="flex w-[59%] items-center pl-[2.4%] text-xs text-gray-600">
<span> Title </span>
</div>
<div class="flex items-center text-xs text-gray-600">
<span> Status </span>
</div>
</div>
<div
ref="rowsContainer"
class=""
@contextmenu.prevent="onOpenContextMenu"
@mouseleave="onListLeave"
>
<ProjectRow
v-for="row in projectStore.projects"
:project="row"
:key="row.id"
:selected="selectedProjectsIds.includes(row.id)"
@mouseenter="hoveredProjectId = row.id"
@update:selected="updateSelection(row.id)"
@update="onUpdateProject(row.id, $event)"
@setStatus="onSetStatus()"
@contextmenu="onOpenContextMenu"
></ProjectRow>
</div>
</div>
<teleport to="body">
<ContextMenu
ref="contextMenuEl"
v-model:show="contextMenuOpen"
:options="[
{ id: nanoid(3), label: 'Delete', kbd: '⌘ ⌫', value: 'del' },
{ id: nanoid(3), label: 'Set status', kbd: 'S', value: 'stat' },
]"
class="absolute"
:style="{
top: `${clickY}px`,
left: `${clickX}px`,
}"
@option="onSelectOption"
></ContextMenu>
</teleport>
<ProjectStatusModal></ProjectStatusModal>
<teleport to="body">
<ContextMenu ref="contextMenuEl" v-model:show="contextMenuOpen" :options="[
{ id: nanoid(3), label: 'Delete', kbd: '⌘ ⌫', value: 'del' },
{ id: nanoid(3), label: 'Set status', kbd: 'S', value: 'stat' },
]" class="absolute" :style="{
top: `${clickY}px`,
left: `${clickX}px`,
}" @option="onSelectOption"></ContextMenu>
</teleport>
<ProjectStatusModal></ProjectStatusModal>
</template>
@store/select-modal