Run prettier && change cors

This commit is contained in:
gregortokarev 2024-08-01 08:45:58 +03:00
parent 78c59e0d4c
commit f1af725349
9 changed files with 483 additions and 413 deletions

View File

@ -5,7 +5,6 @@
"workspaces": [
"packages/*"
],
"packageManager": "npm@9.5.1",
"dependencies": {
"@sentry/vite-plugin": "^2.21.1",

View File

@ -3,32 +3,37 @@ import { ApiKey } from "contract-models";
import { useI18n } from "vue-i18n";
defineProps<{
apiKey: ApiKey;
apiKey: ApiKey;
}>();
const emit = defineEmits<{
(e: "revoke", value: void): void;
(e: "revoke", value: void): void;
}>();
const { t } = useI18n({
messages: {
en: {
revoke: "Revoke"
},
ru: {
revoke: "Отозвать"
}
}
})
messages: {
en: {
revoke: "Revoke",
},
ru: {
revoke: "Отозвать",
},
},
});
</script>
<template>
<div class="flex items-center justify-between rounded bg-gray-100 px-2 py-1 text-black">
<span>{{ apiKey.label }}</span>
<button class="border-gray-150 rounded border px-2 py-[3px] text-xs" @click="emit('revoke')">
{{ t("revoke") }}
</button>
</div>
<div
class="flex items-center justify-between rounded bg-gray-100 px-2 py-1 text-black"
>
<span>{{ apiKey.label }}</span>
<button
class="border-gray-150 rounded border px-2 py-[3px] text-xs"
@click="emit('revoke')"
>
{{ t("revoke") }}
</button>
</div>
</template>
<style scoped></style>

View File

@ -13,33 +13,33 @@ import { i18n, trpc } from "../main.ts";
import { useI18n } from "vue-i18n";
const { t } = useI18n({
messages: {
ru: {
plan: "План",
inbox: "Входящие",
projects: "Проекты",
integrations: "Интеграции",
logout: "Выйти",
profile: "Профиль",
syncing: "Синхронизация",
messages: {
ru: {
plan: "План",
inbox: "Входящие",
projects: "Проекты",
integrations: "Интеграции",
logout: "Выйти",
profile: "Профиль",
syncing: "Синхронизация",
syncNote: `Ваши {syncCount} несохраненные изменения будут загружены когда вы вернетесь в онлайн
syncNote: `Ваши {syncCount} несохраненные изменения будут загружены когда вы вернетесь в онлайн
<br />
<br />
<span class="text-gray-300">Кстати:</span> они перезапушут ваши изменения с других устройств
`,
chooseLang: "Выбрать язык",
},
en: {
plan: "Plan",
inbox: "Inbox",
projects: "Projects",
integrations: "Integrations",
logout: "Logout",
profile: "Open profile",
syncing: "Syncing",
syncNote: `Your {syncCount} unsaved changes will be loaded when you regain
chooseLang: "Выбрать язык",
},
en: {
plan: "Plan",
inbox: "Inbox",
projects: "Projects",
integrations: "Integrations",
logout: "Logout",
profile: "Open profile",
syncing: "Syncing",
syncNote: `Your {syncCount} unsaved changes will be loaded when you regain
connectivity
<br />
<br />
@ -48,35 +48,35 @@ const { t } = useI18n({
`,
chooseLang: "Choose language",
},
chooseLang: "Choose language",
},
},
});
const navItems = ref([
{
title: t("plan"),
icon: "plan",
link: "/",
hint: "⌘ + G",
},
{
title: t("inbox"),
icon: "inbox",
link: "/inbox",
hint: "⌘ + I",
},
{
title: t("projects"),
icon: "folder",
link: "/projects",
hint: "⌘ + P",
},
{
title: t("integrations"),
icon: "integrations",
link: "/integrations",
},
{
title: t("plan"),
icon: "plan",
link: "/",
hint: "⌘ + G",
},
{
title: t("inbox"),
icon: "inbox",
link: "/inbox",
hint: "⌘ + I",
},
{
title: t("projects"),
icon: "folder",
link: "/projects",
hint: "⌘ + P",
},
{
title: t("integrations"),
icon: "integrations",
link: "/integrations",
},
]);
const userStore = useUserStore();
@ -84,19 +84,19 @@ const breakpoints = useBreakpoints(breakpointsTailwind);
const router = useRouter();
onMounted(() => {
hotkeys("[", onBracket);
hotkeys("[", onBracket);
window.addEventListener("resize", (_) => {
compact.value = breakpoints.isSmaller("xl");
});
window.addEventListener("resize", (_) => {
compact.value = breakpoints.isSmaller("xl");
});
});
onUnmounted(() => {
hotkeys.unbind("[", onBracket);
hotkeys.unbind("[", onBracket);
});
function onBracket() {
compact.value = !compact.value;
compact.value = !compact.value;
}
const compact = ref(breakpoints.isSmaller("xl"));
@ -105,49 +105,49 @@ const openPanel = ref(false);
const panelEl = ref<HTMLElement | null>(null);
const userEl = ref<HTMLElement | null>(null);
onClickOutside(
panelEl,
() => {
openPanel.value = false;
},
{ ignore: [userEl] },
panelEl,
() => {
openPanel.value = false;
},
{ ignore: [userEl] },
);
async function gotoProfile() {
await router.push("/profile");
openPanel.value = false;
await router.push("/profile");
openPanel.value = false;
}
function deleteCookie(name: string) {
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
document.cookie = name + "=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/";
}
async function onSignOut() {
await trpc.auth.logout.mutate();
await trpc.auth.logout.mutate();
deleteCookie("session");
location.href = "https://cubicdone.com"
deleteCookie("session");
location.href = "https://cubicdone.com";
}
const { syncCount, networkState } = useSyncState();
const offlineBadge = ref<HTMLElement | null>(null);
const offlineBadgeBound = computed(() => {
if (!offlineBadge.value) return;
return offlineBadge.value.getBoundingClientRect();
if (!offlineBadge.value) return;
return offlineBadge.value.getBoundingClientRect();
});
const offlineHovered = ref(false);
function onChangeOfflineHovered(value: boolean) {
if (!syncing.value) {
offlineHovered.value = value;
}
if (!syncing.value) {
offlineHovered.value = value;
}
}
const syncing = computed(() => {
return networkState.value === "online" && syncCount.value > 0;
return networkState.value === "online" && syncCount.value > 0;
});
const showBadge = computed(() => {
return networkState.value === "offline" || syncing.value;
return networkState.value === "offline" || syncing.value;
});
// locale modal
@ -155,124 +155,180 @@ const localeQuery = ref("");
const locale = useLocalStorage("chosen_locale", i18n.global.locale.value);
const localeOptions = [
{ text: "Русский", id: "ru", icon: "🇷🇺" },
{ text: "English", id: "en", icon: "🇺🇸" },
{ text: "Русский", id: "ru", icon: "🇷🇺" },
{ text: "English", id: "en", icon: "🇺🇸" },
];
const currentLocale = computed(() => {
return localeOptions.find((l) => l.id === locale.value);
return localeOptions.find((l) => l.id === locale.value);
});
const localeOpen = ref(false);
const fuse = new Fuse(localeOptions, { keys: ["text", "id"] });
const filteredOptions = computed(() => {
return localeQuery.value
? fuse.search(localeQuery.value).map((r) => r.item)
: localeOptions;
return localeQuery.value
? fuse.search(localeQuery.value).map((r) => r.item)
: localeOptions;
});
const checkedIndex = computed(() => {
return localeOptions.findIndex((l) => l.id === locale.value);
return localeOptions.findIndex((l) => l.id === locale.value);
});
function onLocaleSelect(id: (typeof localeOptions)[0]["id"]) {
locale.value = id;
location.reload(); // app will take locale on boot
locale.value = id;
location.reload(); // app will take locale on boot
}
</script>
<template>
<div class="flex w-[250px] flex-col rounded-br-2xl rounded-tr-2xl bg-black px-[18px] py-4"
:class="{ '!w-[60px] !px-[13px]': compact }">
<!-- title block-->
<div class="relative flex items-center justify-between">
<div class="flex items-center space-x-1.5" :class="{
'max-w-[85%]': !compact,
'w-full': compact,
}">
<div v-if="userStore.user" ref="userEl" @click="openPanel = !openPanel"
class="!hover:text-white flex max-w-full cursor-pointer items-center from-[#1A1A1A] to-[#141414] transition-colors hover:bg-gradient-to-r"
:class="{
'space-x-2 px-[1px] py-0.5': !compact,
}">
<img :src="userStore.user?.avatar ?? ''" :alt="userStore?.user?.firstName ?? 'profile image'"
class="!min-h-[34px] !w-[34px] !min-w-[34px] shrink-0 overflow-hidden rounded-full" />
<p v-if="!compact && !showBadge"
class="overflow-hidden text-ellipsis whitespace-nowrap text-gray-200">
{{ userStore.user.firstName }} {{ userStore.user.lastName }}
</p>
</div>
<div ref="offlineBadge" v-if="!compact && showBadge"
class="flex items-center space-x-1.5 rounded-md bg-gray-900 px-2 py-1.5 text-[12px] text-gray-500"
@mouseenter="onChangeOfflineHovered(true)" @mouseleave="onChangeOfflineHovered(false)">
<template v-if="!syncing">
<Icon name="offline"></Icon>
<span>offline {{ syncCount }}</span>
</template>
<template v-else>
<Icon name="sync" class="animate-spin text-gray-300"></Icon>
<span class="text-gray-300">{{ t("syncing") }}...</span>
</template>
</div>
</div>
<Icon v-if="!compact" v-hint="'['" name="sidebar-left" class="cursor-pointer text-gray-200"
@click="compact = !compact">
</Icon>
<div ref="panelEl"
class="absolute bottom-0 left-0 w-[200px] translate-y-[calc(100%+4px)] space-y-2 rounded-md border border-gray-900 bg-gradient-to-r from-[#1A1A1A] to-[#141414] px-2 py-1.5 z-10"
v-if="openPanel">
<button @click="gotoProfile"
class="flex w-full cursor-pointer items-center justify-between rounded px-2 py-1.5 text-[14px] text-gray-200 transition-colors hover:bg-black hover:text-gray-50">
<span class="">{{ t("profile") }}</span>
<span> + O</span>
</button>
<button @click="onSignOut"
class="flex w-full cursor-pointer items-center justify-between rounded px-2 py-1.5 text-[14px] text-red-400 transition-colors hover:bg-black">
<span class="">{{ t("logout") }}</span>
<Icon name="exit" class="!h-4 !w-4"></Icon>
</button>
</div>
<div
class="flex w-[250px] flex-col rounded-br-2xl rounded-tr-2xl bg-black px-[18px] py-4"
:class="{ '!w-[60px] !px-[13px]': compact }"
>
<!-- title block-->
<div class="relative flex items-center justify-between">
<div
class="flex items-center space-x-1.5"
:class="{
'max-w-[85%]': !compact,
'w-full': compact,
}"
>
<div
v-if="userStore.user"
ref="userEl"
@click="openPanel = !openPanel"
class="!hover:text-white flex max-w-full cursor-pointer items-center from-[#1A1A1A] to-[#141414] transition-colors hover:bg-gradient-to-r"
:class="{
'space-x-2 px-[1px] py-0.5': !compact,
}"
>
<img
:src="userStore.user?.avatar ?? ''"
:alt="userStore?.user?.firstName ?? 'profile image'"
class="!min-h-[34px] !w-[34px] !min-w-[34px] shrink-0 overflow-hidden rounded-full"
/>
<p
v-if="!compact && !showBadge"
class="overflow-hidden text-ellipsis whitespace-nowrap text-gray-200"
>
{{ userStore.user.firstName }} {{ userStore.user.lastName }}
</p>
</div>
<div v-if="compact && showBadge" ref="offlineBadge" @mouseenter="onChangeOfflineHovered(true)"
@mouseleave="onChangeOfflineHovered(false)"
class="mt-3 flex h-[32px] w-[32px] items-center justify-center rounded bg-gray-900 text-gray-500">
<Icon v-if="!syncing" name="offline"></Icon>
<Icon v-else name="sync" class="animate-spin text-gray-300"></Icon>
<div
ref="offlineBadge"
v-if="!compact && showBadge"
class="flex items-center space-x-1.5 rounded-md bg-gray-900 px-2 py-1.5 text-[12px] text-gray-500"
@mouseenter="onChangeOfflineHovered(true)"
@mouseleave="onChangeOfflineHovered(false)"
>
<template v-if="!syncing">
<Icon name="offline"></Icon>
<span>offline {{ syncCount }}</span>
</template>
<template v-else>
<Icon name="sync" class="animate-spin text-gray-300"></Icon>
<span class="text-gray-300">{{ t("syncing") }}...</span>
</template>
</div>
<!-- navigation block-->
<nav class="mt-6 space-y-1.5">
<router-link class="flex cursor-pointer items-center space-x-2 rounded px-1.5 py-1 text-base text-gray-200"
:class="{
'h-unset w-full': !compact,
'h-[32px] w-[32px] justify-center': compact,
}" v-for="item in navItems" :to="item.link" v-hint="item.hint" :aria-label="item.title"
exact-active-class="!text-white bg-gradient-to-r from-[#1A1A1A] to-[#141414]">
<Icon :size="18" :name="item.icon"></Icon>
<span v-if="!compact">{{ item.title }}</span>
</router-link>
</nav>
<div class="mt-auto flex cursor-pointer items-center space-x-2 rounded from-[#1A1A1A] to-[#141414] px-1.5 py-2 text-base text-gray-200 transition-colors hover:bg-gradient-to-r hover:text-white"
:class="{
'h-unset w-full': !compact,
'h-[32px] w-[32px] justify-center': compact,
}" @click="localeOpen = !localeOpen">
<span>{{ currentLocale?.icon }}</span>
<span v-if="!compact">{{ currentLocale?.text }}</span>
</div>
<teleport to="body">
<div class="absolute pt-1" @mouseenter="offlineHovered = true" @mouseleave="offlineHovered = false"
v-if="offlineBadgeBound && offlineHovered" :style="{
top: `${offlineBadgeBound.top + offlineBadgeBound.height}px`,
left: `${offlineBadgeBound.left}px`,
}">
<p class="w-[215px] rounded-md bg-gray-900 px-2.5 pb-4 pt-2 text-[12px] text-gray-500"
v-html="t('syncNote', { syncCount: syncCount })"></p>
</div>
</teleport>
<SelectModal v-if="filteredOptions" :hint-text="t('chooseLang')" :checked-index="checkedIndex"
:options="filteredOptions" v-model:open="localeOpen" v-model:query="localeQuery" @submit="onLocaleSelect">
</SelectModal>
</div>
<Icon
v-if="!compact"
v-hint="'['"
name="sidebar-left"
class="cursor-pointer text-gray-200"
@click="compact = !compact"
>
</Icon>
<div
ref="panelEl"
class="absolute bottom-0 left-0 z-10 w-[200px] translate-y-[calc(100%+4px)] space-y-2 rounded-md border border-gray-900 bg-gradient-to-r from-[#1A1A1A] to-[#141414] px-2 py-1.5"
v-if="openPanel"
>
<button
@click="gotoProfile"
class="flex w-full cursor-pointer items-center justify-between rounded px-2 py-1.5 text-[14px] text-gray-200 transition-colors hover:bg-black hover:text-gray-50"
>
<span class="">{{ t("profile") }}</span>
<span> + O</span>
</button>
<button
@click="onSignOut"
class="flex w-full cursor-pointer items-center justify-between rounded px-2 py-1.5 text-[14px] text-red-400 transition-colors hover:bg-black"
>
<span class="">{{ t("logout") }}</span>
<Icon name="exit" class="!h-4 !w-4"></Icon>
</button>
</div>
</div>
<div
v-if="compact && showBadge"
ref="offlineBadge"
@mouseenter="onChangeOfflineHovered(true)"
@mouseleave="onChangeOfflineHovered(false)"
class="mt-3 flex h-[32px] w-[32px] items-center justify-center rounded bg-gray-900 text-gray-500"
>
<Icon v-if="!syncing" name="offline"></Icon>
<Icon v-else name="sync" class="animate-spin text-gray-300"></Icon>
</div>
<!-- navigation block-->
<nav class="mt-6 space-y-1.5">
<router-link
class="flex cursor-pointer items-center space-x-2 rounded px-1.5 py-1 text-base text-gray-200"
:class="{
'h-unset w-full': !compact,
'h-[32px] w-[32px] justify-center': compact,
}"
v-for="item in navItems"
:to="item.link"
v-hint="item.hint"
:aria-label="item.title"
exact-active-class="!text-white bg-gradient-to-r from-[#1A1A1A] to-[#141414]"
>
<Icon :size="18" :name="item.icon"></Icon>
<span v-if="!compact">{{ item.title }}</span>
</router-link>
</nav>
<div
class="mt-auto flex cursor-pointer items-center space-x-2 rounded from-[#1A1A1A] to-[#141414] px-1.5 py-2 text-base text-gray-200 transition-colors hover:bg-gradient-to-r hover:text-white"
:class="{
'h-unset w-full': !compact,
'h-[32px] w-[32px] justify-center': compact,
}"
@click="localeOpen = !localeOpen"
>
<span>{{ currentLocale?.icon }}</span>
<span v-if="!compact">{{ currentLocale?.text }}</span>
</div>
<teleport to="body">
<div
class="absolute pt-1"
@mouseenter="offlineHovered = true"
@mouseleave="offlineHovered = false"
v-if="offlineBadgeBound && offlineHovered"
:style="{
top: `${offlineBadgeBound.top + offlineBadgeBound.height}px`,
left: `${offlineBadgeBound.left}px`,
}"
>
<p
class="w-[215px] rounded-md bg-gray-900 px-2.5 pb-4 pt-2 text-[12px] text-gray-500"
v-html="t('syncNote', { syncCount: syncCount })"
></p>
</div>
</teleport>
<SelectModal
v-if="filteredOptions"
:hint-text="t('chooseLang')"
:checked-index="checkedIndex"
:options="filteredOptions"
v-model:open="localeOpen"
v-model:query="localeQuery"
@submit="onLocaleSelect"
>
</SelectModal>
</div>
</template>
<style scoped></style>

View File

@ -5,38 +5,49 @@ import ProjectTag from "../UI/ProjectTag.vue";
import Markdown from "@components/Markdown.vue";
defineProps<{
task: Task;
task: Task;
}>();
const emit = defineEmits<{
(e: "update:status", value: Task["status"]): void;
(e: "update:status", value: Task["status"]): void;
}>();
</script>
<template>
<div class="flex cursor-grab items-start space-x-2 rounded-lg bg-gray-100 px-4 py-2.5 active:cursor-grabbing transition-colors hover:bg-gray-50"
:class="{ 'opacity-50 hover:!bg-gray-100': task.status === 'done' }">
<div class="flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center break-words rounded-full border-2 border-gray-600 text-white"
@click="emit('update:status', task.status === 'done' ? 'todo' : 'done')"
:class="{ '!border-black bg-black': task.status === 'done' }">
<Icon v-if="task.status === 'done'" name="double-check"></Icon>
</div>
<div class="flex flex-col items-start space-y-1.5">
<Markdown :model-value="task.title"></Markdown>
<div class="flex space-x-2">
<ProjectTag v-if="task.projectId" :project-id="task.projectId"></ProjectTag>
<a v-if="task.external" class="flex items-center space-x-1 rounded-lg bg-gray-400 px-1.5 py-1"
:href="task.external.link" target="_blank">
<img class="h-[14px] w-[14px]" :src="task.external.iconURL" alt="" />
<span class="text-xs">{{
task.external.projectTitle
? task.external.projectTitle
: task.external.integrationName
}}</span>
</a>
</div>
</div>
<div
class="flex cursor-grab items-start space-x-2 rounded-lg bg-gray-100 px-4 py-2.5 transition-colors hover:bg-gray-50 active:cursor-grabbing"
:class="{ 'opacity-50 hover:!bg-gray-100': task.status === 'done' }"
>
<div
class="flex h-5 w-5 shrink-0 cursor-pointer items-center justify-center break-words rounded-full border-2 border-gray-600 text-white"
@click="emit('update:status', task.status === 'done' ? 'todo' : 'done')"
:class="{ '!border-black bg-black': task.status === 'done' }"
>
<Icon v-if="task.status === 'done'" name="double-check"></Icon>
</div>
<div class="flex flex-col items-start space-y-1.5">
<Markdown :model-value="task.title"></Markdown>
<div class="flex space-x-2">
<ProjectTag
v-if="task.projectId"
:project-id="task.projectId"
></ProjectTag>
<a
v-if="task.external"
class="flex items-center space-x-1 rounded-lg bg-gray-400 px-1.5 py-1"
:href="task.external.link"
target="_blank"
>
<img class="h-[14px] w-[14px]" :src="task.external.iconURL" alt="" />
<span class="text-xs">{{
task.external.projectTitle
? task.external.projectTitle
: task.external.integrationName
}}</span>
</a>
</div>
</div>
</div>
</template>
<style scoped></style>

View File

@ -23,113 +23,113 @@ import "./style.css";
const pinia = createPinia();
export const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: import.meta.env.PROD
? import.meta.env.VITE_SYNC_URL
: "http://localhost:4000",
fetch(url, options) {
const cookies = cookie.parse(document.cookie);
const sessionToken = cookies["session"];
links: [
httpBatchLink({
url: import.meta.env.PROD
? import.meta.env.VITE_SYNC_URL
: "http://localhost:4000",
fetch(url, options) {
const cookies = cookie.parse(document.cookie);
const sessionToken = cookies["session"];
return fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: sessionToken,
},
});
},
}),
],
return fetch(url, {
...options,
headers: {
...options?.headers,
Authorization: sessionToken,
},
});
},
}),
],
});
const app = createApp(App);
Sentry.init({
app,
dsn: "https://b2a7371c480bdfb4603b8e8189a46d35@o1186023.ingest.us.sentry.io/4507594362585088",
integrations: [
Sentry.feedbackIntegration({
colorScheme: "system",
isEmailRequired: true,
}),
Sentry.replayIntegration(),
Sentry.browserTracingIntegration(),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ["localhost", /^https:\/\/api.cubicdone\.com/],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
app,
dsn: "https://b2a7371c480bdfb4603b8e8189a46d35@o1186023.ingest.us.sentry.io/4507594362585088",
integrations: [
Sentry.feedbackIntegration({
colorScheme: "system",
isEmailRequired: true,
}),
Sentry.replayIntegration(),
Sentry.browserTracingIntegration(),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ["localhost", /^https:\/\/api.cubicdone\.com/],
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
});
app
.directive("hint", hint)
.use(pinia)
.use(vueSyncClientPlugin, {
dbVersion: 11,
schema: [
taskStore,
apiKeyStore,
draftStore,
projectStore,
projectStatusStore,
],
onSync: async (sync, resolveFn) => {
if (sync.targetTable === draftStore.name) {
if (sync.action.actionName === "create")
await trpc.draft.create.mutate(sync.action.data);
else if (sync.action.actionName === "update")
await trpc.draft.update.mutate(sync.action.data);
else if (sync.action.actionName === "delete")
await trpc.draft.delete.mutate(sync.action.id as string);
} else if (sync.targetTable === taskStore.name) {
if (sync.action.actionName === "create")
await trpc.task.create.mutate(sync.action.data);
else if (sync.action.actionName === "update")
await trpc.task.update.mutate(sync.action.data);
else if (sync.action.actionName === "delete")
await trpc.task.delete.mutate(sync.action.id as string);
} else if (sync.targetTable === apiKeyStore.name) {
if (sync.action.actionName === "create")
await trpc.apiKey.create.mutate(sync.action.data);
else if (sync.action.actionName === "update")
await trpc.apiKey.update.mutate(sync.action.data);
else if (sync.action.actionName === "delete")
await trpc.apiKey.delete.mutate(sync.action.id as string);
} else if (sync.targetTable === projectStore.name) {
if (sync.action.actionName === "create")
await trpc.project.create.mutate(sync.action.data);
else if (sync.action.actionName === "update")
await trpc.project.update.mutate(sync.action.data);
else if (sync.action.actionName === "delete")
await trpc.project.delete.mutate(sync.action.id as string);
}
resolveFn();
},
})
.use(router);
.directive("hint", hint)
.use(pinia)
.use(vueSyncClientPlugin, {
dbVersion: 11,
schema: [
taskStore,
apiKeyStore,
draftStore,
projectStore,
projectStatusStore,
],
onSync: async (sync, resolveFn) => {
if (sync.targetTable === draftStore.name) {
if (sync.action.actionName === "create")
await trpc.draft.create.mutate(sync.action.data);
else if (sync.action.actionName === "update")
await trpc.draft.update.mutate(sync.action.data);
else if (sync.action.actionName === "delete")
await trpc.draft.delete.mutate(sync.action.id as string);
} else if (sync.targetTable === taskStore.name) {
if (sync.action.actionName === "create")
await trpc.task.create.mutate(sync.action.data);
else if (sync.action.actionName === "update")
await trpc.task.update.mutate(sync.action.data);
else if (sync.action.actionName === "delete")
await trpc.task.delete.mutate(sync.action.id as string);
} else if (sync.targetTable === apiKeyStore.name) {
if (sync.action.actionName === "create")
await trpc.apiKey.create.mutate(sync.action.data);
else if (sync.action.actionName === "update")
await trpc.apiKey.update.mutate(sync.action.data);
else if (sync.action.actionName === "delete")
await trpc.apiKey.delete.mutate(sync.action.id as string);
} else if (sync.targetTable === projectStore.name) {
if (sync.action.actionName === "create")
await trpc.project.create.mutate(sync.action.data);
else if (sync.action.actionName === "update")
await trpc.project.update.mutate(sync.action.data);
else if (sync.action.actionName === "delete")
await trpc.project.delete.mutate(sync.action.id as string);
}
resolveFn();
},
})
.use(router);
function customRule(choice: number, _choicesLength: number) {
const calcValue = Math.abs(choice) % 100;
const num = calcValue % 10;
const calcValue = Math.abs(choice) % 100;
const num = calcValue % 10;
if (calcValue > 10 && calcValue < 20) return 3;
if (num > 1 && num < 5) return 2;
if (num === 1) return 1;
return 3;
if (calcValue > 10 && calcValue < 20) return 3;
if (num > 1 && num < 5) return 2;
if (num === 1) return 1;
return 3;
}
export const i18n = createI18n({
locale: "ru",
fallbackLocale: "en",
legacy: false,
pluralizationRules: {
ru: customRule,
},
locale: "ru",
fallbackLocale: "en",
legacy: false,
pluralizationRules: {
ru: customRule,
},
});
app.use(i18n);

View File

@ -7,48 +7,48 @@ import { VitePWA } from "vite-plugin-pwa";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
createSVGSpritePlugin({
svgFolder: "./src/assets/svg",
transformIndexHtmlTag: {
injectTo: "body",
},
}),
VitePWA({
registerType: "autoUpdate",
injectRegister: "auto",
workbox: { globPatterns: ["**/*.{js,css,html,ico,png,svg}"] },
}),
sentryVitePlugin({
org: "me-sgp",
project: "cubicdone",
}),
plugins: [
vue(),
createSVGSpritePlugin({
svgFolder: "./src/assets/svg",
transformIndexHtmlTag: {
injectTo: "body",
},
}),
VitePWA({
registerType: "autoUpdate",
injectRegister: "auto",
workbox: { globPatterns: ["**/*.{js,css,html,ico,png,svg}"] },
}),
sentryVitePlugin({
org: "me-sgp",
project: "cubicdone",
}),
],
resolve: {
alias: [
{
find: "@components",
replacement: path.resolve(__dirname, "./src/components"),
},
{ find: "@store", replacement: path.resolve(__dirname, "./src/store") },
{ find: "@models", replacement: path.resolve(__dirname, "./src/models") },
{ find: "@utils", replacement: path.resolve(__dirname, "./src/utils") },
{ find: "@assets", replacement: path.resolve(__dirname, "./src/assets") },
],
resolve: {
alias: [
{
find: "@components",
replacement: path.resolve(__dirname, "./src/components"),
},
{ find: "@store", replacement: path.resolve(__dirname, "./src/store") },
{ find: "@models", replacement: path.resolve(__dirname, "./src/models") },
{ find: "@utils", replacement: path.resolve(__dirname, "./src/utils") },
{ find: "@assets", replacement: path.resolve(__dirname, "./src/assets") },
],
},
},
build: {
target: "esnext",
sourcemap: true,
},
build: {
target: "esnext",
sourcemap: true,
},
server: {
port: 3000,
host: true,
},
preview: {
port: 3000,
host: true,
},
server: {
port: 3000,
host: true,
},
preview: {
port: 3000,
host: true,
},
});

View File

@ -1,31 +1,30 @@
import { createContext, router } from "./trpc";
import cors from "cors";
import { drafts } from "./router/draft.router";
import { tasks } from "./router/tasks.router";
import { projects } from "./router/project.router";
import { apiKeys } from "./router/apikey.router";
import { projectStatus } from "./router/project-status.router";
import cookieParser from "cookie-parser";
import cors from "cors";
import { apiKeys } from "./router/apikey.router";
import { drafts } from "./router/draft.router";
import { projectStatus } from "./router/project-status.router";
import { projects } from "./router/project.router";
import { tasks } from "./router/tasks.router";
import { createContext, router } from "./trpc";
import express from "express";
import * as trpcExpress from "@trpc/server/adapters/express";
import express from "express";
import { webcrypto } from "node:crypto";
import { authRouter } from "./router/auth.router";
import {
oauthRedirectRouter,
sameOauthState,
oauthRedirectRouter
} from "./webhooks/oauth-redirects";
import { oauthUrlRouter } from "./webhooks/oauth-url";
globalThis.crypto = webcrypto as Crypto;
const appRouter = router({
draft: drafts,
task: tasks,
project: projects,
apiKey: apiKeys,
projectStatus: projectStatus,
auth: authRouter,
draft: drafts,
task: tasks,
project: projects,
apiKey: apiKeys,
projectStatus: projectStatus,
auth: authRouter,
});
export type AppRouter = typeof appRouter;
@ -33,10 +32,10 @@ export type AppRouter = typeof appRouter;
const app = express();
app.use(
cors({
origin: ["http://localhost:5173", "https://app.cubicdone.com"],
credentials: true,
}),
cors({
origin: ["http://localhost:3000", "https://app.cubicdone.com"],
credentials: true,
}),
);
app.use(cookieParser());
@ -45,7 +44,7 @@ app.use("/oauth/redirect", oauthRedirectRouter);
app.use("/oauth", oauthUrlRouter);
app.use(
trpcExpress.createExpressMiddleware({ router: appRouter, createContext }),
trpcExpress.createExpressMiddleware({ router: appRouter, createContext }),
);
app.listen(4000);

View File

@ -1,64 +1,64 @@
export const languages = {
en: "English",
ru: "Русский",
en: "English",
ru: "Русский",
};
export const defaultLang = "ru";
export const ui = {
en: {
"nav.pricing": "Pricing",
"nav.signup": "Sign up",
"nav.login": "Log in",
en: {
"nav.pricing": "Pricing",
"nav.signup": "Sign up",
"nav.login": "Log in",
"hero.title": `Do not spend days to create
"hero.title": `Do not spend days to create
<span class="animated-gradient-text">productivity system</span>, we already do that`,
"hero.subtitle":
"TODO List with preconfigured productivity system, that will keep your tasks and mind in shape",
"hero.action": "Try it now!",
"hero.subtitle":
"TODO List with preconfigured productivity system, that will keep your tasks and mind in shape",
"hero.action": "Try it now!",
"footer.subtitle":
"TODO List with preconfigured productivity system, that will keep your tasks and mind in shape",
"footer.subtitle":
"TODO List with preconfigured productivity system, that will keep your tasks and mind in shape",
"footer.company": "Company",
"footer.about": "About us",
"footer.sitemap": "Sitemap",
"footer.company": "Company",
"footer.about": "About us",
"footer.sitemap": "Sitemap",
"footer.legal": "Legal",
"footer.privacy": "Privacy",
"footer.terms": "Terms of service",
"footer.legal": "Legal",
"footer.privacy": "Privacy",
"footer.terms": "Terms of service",
"footer.support": "Support",
"footer.faq": "FAQ",
"footer.contact": "Contact",
"footer.support": "Support",
"footer.faq": "FAQ",
"footer.contact": "Contact",
askquestion: "Ask a question",
},
ru: {
"nav.pricing": "Стоимость",
"nav.signup": "Регистрация",
"nav.login": "Вход",
askquestion: "Ask a question",
},
ru: {
"nav.pricing": "Стоимость",
"nav.signup": "Регистрация",
"nav.login": "Вход",
"hero.title": `Не тратьте дни на создание <span class="animated-gradient-text">Системы продуктивности</span>, мы уже сделали это за вас`,
"hero.subtitle":
"Список дел с подготовленной системой продуктивности, которая сохранит ваши задачи и мышление в тонусе",
"hero.action": "Пробовать сейчас!",
"hero.title": `Не тратьте дни на создание <span class="animated-gradient-text">Системы продуктивности</span>, мы уже сделали это за вас`,
"hero.subtitle":
"Список дел с подготовленной системой продуктивности, которая сохранит ваши задачи и мышление в тонусе",
"hero.action": "Пробовать сейчас!",
"footer.subtitle":
"Список дел с подготовленной системой продуктивности, которая сохранит ваши задачи и мышление в тонусе",
"footer.subtitle":
"Список дел с подготовленной системой продуктивности, которая сохранит ваши задачи и мышление в тонусе",
"footer.company": "Компания",
"footer.about": "О нас",
"footer.sitemap": "Sitemap",
"footer.company": "Компания",
"footer.about": "О нас",
"footer.sitemap": "Sitemap",
"footer.legal": "Юр. вопросы",
"footer.privacy": "Приватность",
"footer.terms": "Условия использования",
"footer.legal": "Юр. вопросы",
"footer.privacy": "Приватность",
"footer.terms": "Условия использования",
"footer.support": "Поддержка",
"footer.faq": "FAQ",
"footer.contact": "Контакты",
"footer.support": "Поддержка",
"footer.faq": "FAQ",
"footer.contact": "Контакты",
askquestion: "Задать вопрос",
},
askquestion: "Задать вопрос",
},
} as const;

View File

@ -1,13 +1,13 @@
import { defaultLang, ui } from "./ui";
export function getLangFromUrl(url: URL) {
const [, lang] = url.pathname.split("/");
if (lang! in ui) return lang as keyof typeof ui;
return defaultLang;
const [, lang] = url.pathname.split("/");
if (lang! in ui) return lang as keyof typeof ui;
return defaultLang;
}
export function useTranslations(lang: keyof typeof ui) {
return function t(key: keyof (typeof ui)[typeof defaultLang]) {
return ui[lang][key] || ui[defaultLang][key];
};
return function t(key: keyof (typeof ui)[typeof defaultLang]) {
return ui[lang][key] || ui[defaultLang][key];
};
}