Merge branch 'develop' into fix/cw-4085

This commit is contained in:
Shivam Mishra
2025-03-20 10:22:49 +05:30
committed by GitHub
595 changed files with 9887 additions and 2291 deletions
@@ -28,8 +28,8 @@ export default {
'border-b border-solid border-n-weak/60 dark:border-n-weak': showBorder,
}"
>
<div class="grid grid-cols-1 lg:grid-cols-6 gap-6">
<div class="col-span-2 xl:col-span-1">
<div class="grid grid-cols-1 lg:grid-cols-8 gap-6">
<div class="col-span-2">
<p
v-if="title"
class="text-base text-woot-500 dark:text-woot-500 mb-0 font-medium"
@@ -48,7 +48,7 @@ export default {
{{ note }}
</p>
</div>
<div class="col-span-4 lg:col-span-4 2xl:col-span-3">
<div class="col-span-6 xl:col-span-5">
<slot />
</div>
</div>
@@ -1,12 +1,47 @@
<script setup>
import { computed } from 'vue';
import { computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import Button from 'dashboard/components-next/button/Button.vue';
import FilterSelect from 'dashboard/components-next/filter/inputs/FilterSelect.vue';
const props = defineProps({
table: {
type: Object,
required: true,
},
defaultPageSize: {
type: Number,
default: 10,
},
showPageSizeSelector: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['pageSizeChange']);
const { t } = useI18n();
const pageSizeOptions = [
{
label: `${t('REPORT.PAGINATION.PER_PAGE_TEMPLATE', { size: 10 })}`,
value: 10,
},
{
label: `${t('REPORT.PAGINATION.PER_PAGE_TEMPLATE', { size: 20 })}`,
value: 20,
},
{
label: `${t('REPORT.PAGINATION.PER_PAGE_TEMPLATE', { size: 50 })}`,
value: 50,
},
{
label: `${t('REPORT.PAGINATION.PER_PAGE_TEMPLATE', { size: 100 })}`,
value: 100,
},
];
const getFormattedPages = (start, end) => {
const formatter = new Intl.NumberFormat(navigator.language);
return Array.from({ length: end - start + 1 }, (_, i) =>
@@ -48,70 +83,98 @@ const end = computed(() => {
total.value
);
});
const currentPageSize = computed({
get() {
return props.table.getState().pagination.pageSize;
},
set(newValue) {
props.table.setPageSize(Number(newValue));
emit('pageSizeChange', Number(newValue));
},
});
onMounted(() => {
if (
props.showPageSizeSelector &&
props.defaultPageSize &&
props.defaultPageSize !== 10
) {
props.table.setPageSize(props.defaultPageSize);
}
});
</script>
<template>
<div class="flex items-center justify-between">
<div class="flex flex-1 items-center justify-between">
<div>
<p class="text-sm text-n-slate-11 mb-0">
{{ $t('REPORT.PAGINATION.RESULTS', { start, end, total }) }}
</p>
</div>
<nav class="isolate inline-flex gap-1">
<woot-button
:disabled="!table.getCanPreviousPage()"
variant="clear"
class="h-8 border-0 flex items-center"
color-scheme="secondary"
@click="table.setPageIndex(0)"
>
<span class="i-lucide-chevrons-left size-3" aria-hidden="true" />
</woot-button>
<woot-button
variant="clear"
class="h-8 border-0 flex items-center"
color-scheme="secondary"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
>
<span class="i-lucide-chevron-left size-3" aria-hidden="true" />
</woot-button>
<woot-button
v-for="page in visiblePages"
:key="page"
variant="clear"
class="h-8 flex items-center justify-center text-xs leading-none text-center"
:class="page == currentPage ? 'border-n-brand' : 'border-slate-50'"
color-scheme="secondary"
@click="table.setPageIndex(page - 1)"
>
<span
class="text-center"
:class="{ 'text-n-brand': page == currentPage }"
<div class="flex flex-1 items-center gap-2 justify-between">
<p class="text-sm truncate text-n-slate-11 mb-0">
{{ $t('REPORT.PAGINATION.RESULTS', { start, end, total }) }}
</p>
<div class="flex items-center gap-2">
<FilterSelect
v-if="showPageSizeSelector"
v-model="currentPageSize"
variant="outline"
hide-icon
class="[&>button]:text-n-slate-11 [&>button]:hover:text-n-slate-12 [&>button]:h-6"
:options="pageSizeOptions"
/>
<nav class="isolate inline-flex items-center gap-1.5">
<Button
icon="i-lucide-chevrons-left"
ghost
slate
sm
class="!size-6"
:disabled="!table.getCanPreviousPage()"
@click="table.setPageIndex(0)"
/>
<Button
icon="i-lucide-chevron-left"
ghost
slate
sm
class="!size-6"
:disabled="!table.getCanPreviousPage()"
@click="table.previousPage()"
/>
<Button
v-for="page in visiblePages"
:key="page"
xs
outline
:color="page == currentPage ? 'blue' : 'slate'"
class="!h-6 min-w-6"
@click="table.setPageIndex(page - 1)"
>
{{ page }}
</span>
</woot-button>
<woot-button
:disabled="!table.getCanNextPage()"
variant="clear"
class="h-8 border-0 flex items-center"
color-scheme="secondary"
@click="table.nextPage()"
>
<span class="i-lucide-chevron-right size-3" aria-hidden="true" />
</woot-button>
<woot-button
:disabled="!table.getCanNextPage()"
variant="clear"
class="h-8 border-0 flex items-center"
color-scheme="secondary"
@click="table.setPageIndex(table.getPageCount() - 1)"
>
<span class="i-lucide-chevrons-right size-3" aria-hidden="true" />
</woot-button>
</nav>
<span
class="text-center"
:class="{ 'text-n-brand': page == currentPage }"
>
{{ page }}
</span>
</Button>
<Button
icon="i-lucide-chevron-right"
ghost
slate
sm
class="!size-6"
:disabled="!table.getCanNextPage()"
@click="table.nextPage()"
/>
<Button
icon="i-lucide-chevrons-right"
ghost
slate
sm
class="!size-6"
:disabled="!table.getCanNextPage()"
@click="table.setPageIndex(table.getPageCount() - 1)"
/>
</nav>
</div>
</div>
</div>
</template>
@@ -68,7 +68,7 @@ export default {
<style lang="scss" scoped>
.wizard-box {
.item {
@apply cursor-pointer after:bg-slate-75 before:bg-slate-75 dark:after:bg-slate-600 dark:before:bg-slate-600 py-4 pr-4 pl-6 relative before:h-4 before:top-0 last:before:h-0 first:before:h-0 last:after:h-0 before:content-[''] before:absolute before:w-0.5 after:content-[''] after:h-full after:absolute after:top-5 after:w-0.5;
@apply cursor-pointer after:bg-slate-75 before:bg-slate-75 dark:after:bg-slate-600 dark:before:bg-slate-600 py-4 ltr:pr-4 rtl:pl-4 ltr:pl-6 rtl:pr-6 relative before:h-4 before:top-0 last:before:h-0 first:before:h-0 last:after:h-0 before:content-[''] before:absolute before:w-0.5 after:content-[''] after:h-full after:absolute after:top-5 after:w-0.5 rtl:after:left-6 rtl:before:left-6;
&.active {
h3 {
@@ -177,6 +177,7 @@ export default {
icon="dismiss"
variant="clear"
color-scheme="secondary"
class="flex-shrink-0"
@click="removeAction"
/>
</div>
@@ -201,10 +202,10 @@ export default {
<style lang="scss" scoped>
.filter {
@apply bg-n-slate-3 dark:bg-n-solid-3 p-2 border border-solid border-n-strong dark:border-n-strong rounded-md mb-2;
@apply bg-n-background p-2 border border-solid border-n-strong dark:border-n-strong rounded-lg mb-2;
&.is-a-macro {
@apply mb-0 bg-n-slate-2 dark:bg-n-solid-3 p-0 border-0 rounded-none;
@apply mb-0 bg-n-background dark:bg-n-solid-1 p-0 border-0 rounded-none;
}
}
@@ -213,15 +214,19 @@ export default {
}
.filter.has-error {
@apply bg-red-50 dark:bg-red-800/50 border-red-100 dark:border-red-700/50;
@apply bg-n-ruby-8/20 border-n-ruby-5 dark:border-n-ruby-5;
&.is-a-macro {
@apply bg-transparent;
}
}
.filter-inputs {
@apply flex;
@apply flex gap-1;
}
.filter-error {
@apply text-red-500 dark:text-red-200 block my-1 mx-0;
@apply text-n-ruby-9 dark:text-n-ruby-9 block my-1 mx-0;
}
.action__question,
@@ -29,7 +29,7 @@ export default {
<template>
<div>
<div class="multiselect-wrap--small">
<div class="multiselect-wrap--small flex flex-col gap-1 mt-1">
<multiselect
v-model="selectedTeams"
track-by="id"
@@ -11,13 +11,13 @@ export default {
<div class="empty-state py-16 px-1 ml-0 mr-0">
<h3
v-if="title"
class="text-slate-700 dark:text-slate-200 block text-center w-full text-xl font-thin"
class="text-n-slate-12 block text-center w-full text-xl font-medium"
>
{{ title }}
</h3>
<p
v-if="message"
class="block text-center text-slate-500 dark:text-slate-400 my-4 mx-auto w-[90%]"
class="block text-center text-n-slate-11 dark:text-slate-400 my-4 mx-auto w-[90%]"
>
{{ message }}
</p>
@@ -127,8 +127,8 @@ export default {
},
getInputErrorClass(errorMessage) {
return errorMessage
? 'bg-red-50 dark:bg-red-800/50 border-red-100 dark:border-red-700/50'
: 'bg-n-slate-3 dark:bg-n-solid-3 border-n-strong dark:border-n-strong';
? 'bg-n-ruby-8/20 border-n-ruby-5 dark:border-n-ruby-5'
: 'bg-n-background border-n-weak dark:border-n-weak';
},
},
};
@@ -138,14 +138,14 @@ export default {
<template>
<div>
<div
class="p-2 border border-solid rounded-md"
class="p-2 border border-solid rounded-lg"
:class="getInputErrorClass(errorMessage)"
>
<div class="flex">
<div class="flex gap-1">
<select
v-if="groupedFilters"
v-model="attributeKey"
class="bg-white max-w-[30%] dark:bg-slate-900 mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
class="max-w-[30%] mb-0 mr-1"
@change="resetFilter()"
>
<optgroup
@@ -166,7 +166,7 @@ export default {
<select
v-else
v-model="attributeKey"
class="bg-white max-w-[30%] dark:bg-slate-900 mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
class="max-w-[30%] mb-0 mr-1"
@change="resetFilter()"
>
<option
@@ -179,10 +179,7 @@ export default {
</option>
</select>
<select
v-model="filterOperator"
class="bg-white dark:bg-slate-900 max-w-[20%] mb-0 mr-1 text-slate-800 dark:text-slate-100 border-slate-75 dark:border-slate-600"
>
<select v-model="filterOperator" class="max-w-[20%] mb-0 mr-1">
<option
v-for="(operator, o) in operators"
:key="o"
@@ -234,14 +231,14 @@ export default {
v-model="values"
type="date"
:editable="false"
class="mb-0 datepicker"
class="!mb-0 datepicker"
/>
</div>
<input
v-else
v-model="values"
type="text"
class="mb-0"
class="!mb-0"
:placeholder="$t('FILTER.INPUT_PLACEHOLDER')"
/>
</div>
@@ -287,7 +284,7 @@ export default {
}
.filter-error {
@apply text-red-500 dark:text-red-200 block my-1 mx-0;
@apply text-n-ruby-9 dark:text-n-ruby-9 block my-1 mx-0;
}
.multiselect {
@@ -1,90 +0,0 @@
<script setup>
import { ref, computed } from 'vue';
import { useAdmin } from 'dashboard/composables/useAdmin';
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
import AddLabel from 'shared/components/ui/dropdown/AddLabel.vue';
import LabelDropdown from 'shared/components/ui/label/LabelDropdown.vue';
const props = defineProps({
allLabels: {
type: Array,
default: () => [],
},
savedLabels: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['add', 'remove']);
const { isAdmin } = useAdmin();
const showSearchDropdownLabel = ref(false);
const selectedLabels = computed(() => {
return props.savedLabels.map(label => label.title);
});
const addItem = label => {
emit('add', label);
};
const removeItem = label => {
emit('remove', label);
};
const toggleLabels = () => {
showSearchDropdownLabel.value = !showSearchDropdownLabel.value;
};
const closeDropdownLabel = () => {
showSearchDropdownLabel.value = false;
};
const keyboardEvents = {
KeyL: {
action: e => {
toggleLabels();
e.preventDefault();
},
},
Escape: {
action: () => closeDropdownLabel(),
allowOnFocusedInput: true,
},
};
useKeyboardEvents(keyboardEvents);
</script>
<template>
<div v-on-clickaway="closeDropdownLabel" class="relative leading-6">
<AddLabel @add="toggleLabels" />
<woot-label
v-for="label in savedLabels"
:key="label.id"
:title="label.title"
:description="label.description"
show-close
:color="label.color"
variant="smooth"
@remove="removeItem"
/>
<div class="absolute w-full top-7">
<div
:class="{ 'dropdown-pane--open': showSearchDropdownLabel }"
class="!box-border !w-full dropdown-pane"
>
<LabelDropdown
v-if="showSearchDropdownLabel"
:account-labels="allLabels"
:selected-labels="selectedLabels"
:allow-creation="isAdmin"
@add="addItem"
@remove="removeItem"
/>
</div>
</div>
</div>
</template>
@@ -14,16 +14,18 @@ export default {
</script>
<template>
<div class="pt-4 pb-0 px-8 border-b border-solid border-n-weak/60">
<h2 class="text-2xl text-slate-800 dark:text-slate-100 mb-1 font-medium">
{{ headerTitle }}
</h2>
<p
v-if="headerContent"
class="w-full text-slate-600 dark:text-slate-300 text-sm mb-2"
>
{{ headerContent }}
</p>
<slot />
<div class="border-b border-solid border-n-weak/60">
<div class="max-w-6xl w-full mx-auto pt-4 pb-0 px-8">
<h2 class="text-2xl text-slate-800 dark:text-slate-100 mb-1 font-medium">
{{ headerTitle }}
</h2>
<p
v-if="headerContent"
class="w-full text-slate-600 dark:text-slate-300 text-sm mb-2"
>
{{ headerContent }}
</p>
<slot />
</div>
</div>
</template>
@@ -783,15 +783,15 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
.is-private {
.prosemirror-mention-node {
@apply font-medium bg-n-amber-2/80 dark:bg-n-amber-2/80 text-slate-900 dark:text-slate-25 py-0 px-1;
@apply font-medium bg-n-amber-2/80 dark:bg-n-amber-2/80 text-n-slate-12 py-0 px-1;
}
.ProseMirror-menubar-wrapper {
> .ProseMirror {
@apply text-slate-800 dark:text-slate-25;
@apply text-n-slate-12;
p {
@apply text-slate-800 dark:text-slate-25;
@apply text-n-slate-12;
}
}
}
@@ -802,11 +802,11 @@ useEmitter(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, insertContentIntoEditor);
}
.message-editor {
@apply border border-solid border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-900 rounded-md py-0 px-1 mb-0;
@apply rounded-lg outline outline-1 outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6 bg-n-alpha-black2 py-0 px-1 mb-0;
}
.editor_warning {
@apply border border-solid border-red-400 dark:border-red-400;
@apply outline outline-1 outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9;
}
.editor-warning__message {
@@ -1253,7 +1253,7 @@ export default {
@apply relative py-0 px-4 -mt-px;
textarea {
@apply shadow-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
@apply shadow-none outline-none border-transparent bg-transparent m-0 max-h-60 min-h-[3rem] pt-4 pb-0 px-0 resize-none;
}
}
@@ -97,7 +97,7 @@ export default {
<woot-input
v-model="v$.toEmailsVal.$model"
type="text"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
:class="{ error: v$.toEmailsVal.$error }"
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
@blur="onBlur"
@@ -113,7 +113,7 @@ export default {
<div class="flex-1 min-w-0 m-0 rounded-none whitespace-nowrap">
<woot-input
v-model="v$.ccEmailsVal.$model"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
type="text"
:class="{ error: v$.ccEmailsVal.$error }"
:placeholder="$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.CC.PLACEHOLDER')"
@@ -142,7 +142,7 @@ export default {
<woot-input
v-model="v$.bccEmailsVal.$model"
type="text"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:h-8 [&>input]:text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
class="[&>input]:!mb-0 [&>input]:border-transparent [&>input]:!outline-none [&>input]:h-8 [&>input]:!text-sm [&>input]:!border-0 [&>input]:border-none [&>input]:!bg-transparent dark:[&>input]:!bg-transparent"
:class="{ error: v$.bccEmailsVal.$error }"
:placeholder="
$t('CONVERSATION.REPLYBOX.EMAIL_HEAD.BCC.PLACEHOLDER')
@@ -0,0 +1,105 @@
<script setup>
import { computed } from 'vue';
import { format } from 'date-fns';
import { useI18n } from 'vue-i18n';
const props = defineProps({
order: {
type: Object,
required: true,
},
});
const { t } = useI18n();
const formatDate = dateString => {
return format(new Date(dateString), 'MMM d, yyyy');
};
const formatCurrency = (amount, currency) => {
return new Intl.NumberFormat('en', {
style: 'currency',
currency: currency || 'USD',
}).format(amount);
};
const getStatusClass = status => {
const classes = {
paid: 'bg-n-teal-5 text-n-teal-12',
};
return classes[status] || 'bg-slate-50 text-slate-700';
};
const getStatusI18nKey = (type, status = '') => {
return `CONVERSATION_SIDEBAR.SHOPIFY.${type.toUpperCase()}_STATUS.${status.toUpperCase()}`;
};
const fulfillmentStatus = computed(() => {
const { fulfillment_status: status } = props.order;
if (!status) {
return '';
}
return t(getStatusI18nKey('FULFILLMENT', status));
});
const financialStatus = computed(() => {
const { financial_status: status } = props.order;
if (!status) {
return '';
}
return t(getStatusI18nKey('FINANCIAL', status));
});
const getFulfillmentClass = status => {
const classes = {
fulfilled: 'text-green-600',
partial: 'text-yellow-600',
unfulfilled: 'text-red-600',
};
return classes[status] || 'text-slate-600';
};
</script>
<template>
<div
class="py-3 border-b border-n-weak last:border-b-0 flex flex-col gap-1.5"
>
<div class="flex justify-between items-center">
<div class="font-medium flex">
<a
:href="order.admin_url"
target="_blank"
rel="noopener noreferrer"
class="hover:underline text-n-slate-12 cursor-pointer truncate"
>
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.ORDER_ID', { id: order.id }) }}
<i class="i-lucide-external-link pl-5" />
</a>
</div>
<div
:class="getStatusClass(order.financial_status)"
class="text-xs px-2 py-1 rounded capitalize truncate"
:title="financialStatus"
>
{{ financialStatus }}
</div>
</div>
<div class="text-sm text-n-slate-12">
<span class="text-n-slate-11 border-r border-n-weak pr-2">
{{ formatDate(order.created_at) }}
</span>
<span class="text-n-slate-11 pl-2">
{{ formatCurrency(order.total_price, order.currency) }}
</span>
</div>
<div v-if="fulfillmentStatus">
<span
:class="getFulfillmentClass(order.fulfillment_status)"
class="capitalize font-medium"
:title="fulfillmentStatus"
>
{{ fulfillmentStatus }}
</span>
</div>
</div>
</template>
@@ -0,0 +1,71 @@
<script setup>
import { ref, watch, computed } from 'vue';
import { useFunctionGetter } from 'dashboard/composables/store';
import Spinner from 'dashboard/components-next/spinner/Spinner.vue';
import ShopifyAPI from '../../../api/integrations/shopify';
import ShopifyOrderItem from './ShopifyOrderItem.vue';
const props = defineProps({
contactId: {
type: [Number, String],
required: true,
},
});
const contact = useFunctionGetter('contacts/getContact', props.contactId);
const hasSearchableInfo = computed(
() => !!contact.value?.email || !!contact.value?.phone_number
);
const orders = ref([]);
const loading = ref(true);
const error = ref('');
const fetchOrders = async () => {
try {
loading.value = true;
const response = await ShopifyAPI.getOrders(props.contactId);
orders.value = response.data.orders;
} catch (e) {
error.value =
e.response?.data?.error || 'CONVERSATION_SIDEBAR.SHOPIFY.ERROR';
} finally {
loading.value = false;
}
};
watch(
() => props.contactId,
() => {
if (hasSearchableInfo.value) {
fetchOrders();
}
},
{ immediate: true }
);
</script>
<template>
<div class="px-4 py-2 text-n-slate-12">
<div v-if="!hasSearchableInfo" class="text-center text-n-slate-12">
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
</div>
<div v-else-if="loading" class="flex justify-center items-center p-4">
<Spinner size="32" class="text-n-brand" />
</div>
<div v-else-if="error" class="text-center text-n-ruby-12">
{{ error }}
</div>
<div v-else-if="!orders.length" class="text-center text-n-slate-12">
{{ $t('CONVERSATION_SIDEBAR.SHOPIFY.NO_SHOPIFY_ORDERS') }}
</div>
<div v-else>
<ShopifyOrderItem
v-for="order in orders"
:key="order.id"
:order="order"
/>
</div>
</div>
</template>
@@ -166,188 +166,190 @@ onMounted(() => {
</script>
<template>
<woot-modal
v-model:show="show"
full-width
:show-close-button="false"
:on-close="onClose"
>
<div
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
@click="onClose"
<Teleport to="body">
<woot-modal
v-model:show="show"
full-width
:show-close-button="false"
:on-close="onClose"
>
<header
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
@click.stop
<div
class="bg-n-background flex flex-col h-[inherit] w-[inherit] overflow-hidden select-none"
@click="onClose"
>
<div
v-if="senderDetails"
class="flex items-center min-w-[15rem] shrink-0"
<header
class="z-10 flex items-center justify-between w-full h-16 px-6 py-2 bg-n-background border-b border-n-weak"
@click.stop
>
<Thumbnail
v-if="senderDetails.avatar"
:username="senderDetails.name"
:src="senderDetails.avatar"
class="flex-shrink-0"
/>
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
<h3 class="text-base leading-5 m-0 font-medium">
<span
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
>
{{ senderDetails.name }}
</span>
</h3>
<span
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
>
{{ readableTime }}
</span>
</div>
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
<div class="flex items-center gap-2 ml-2 shrink-0">
<NextButton
v-if="isImage"
icon="i-lucide-zoom-in"
slate
ghost
@click="onZoom(0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-zoom-out"
slate
ghost
@click="onZoom(-0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-ccw"
slate
ghost
@click="onRotate('counter-clockwise')"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-cw"
slate
ghost
@click="onRotate('clockwise')"
/>
<NextButton
icon="i-lucide-download"
slate
ghost
:is-loading="isDownloading"
:disabled="isDownloading"
@click="onClickDownload"
/>
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
</div>
</header>
<main class="flex items-stretch flex-1 h-full overflow-hidden">
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-left"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === 0"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex - 1],
activeImageIndex - 1
)
"
/>
</div>
<div class="flex-1 flex items-center justify-center overflow-hidden">
<div
v-if="isImage"
:style="imageWrapperStyle"
class="flex items-center justify-center origin-center"
:class="{
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
// and prevent image from overflowing container in different aspect ratios
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
activeImageRotation % 180 !== 0,
'size-full': activeImageRotation % 180 === 0,
}"
v-if="senderDetails"
class="flex items-center min-w-[15rem] shrink-0"
>
<img
ref="imageRef"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
:style="imageStyle"
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
@click.stop
@dblclick.stop="onDoubleClickZoomImage"
@wheel.prevent.stop="onWheelImageZoom"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
<Thumbnail
v-if="senderDetails.avatar"
:username="senderDetails.name"
:src="senderDetails.avatar"
class="flex-shrink-0"
/>
<div class="flex flex-col ml-2 rtl:ml-0 rtl:mr-2 overflow-hidden">
<h3 class="text-base leading-5 m-0 font-medium">
<span
class="overflow-hidden text-n-slate-12 whitespace-nowrap text-ellipsis"
>
{{ senderDetails.name }}
</span>
</h3>
<span
class="text-xs text-n-slate-11 whitespace-nowrap text-ellipsis"
>
{{ readableTime }}
</span>
</div>
</div>
<div
class="flex-1 mx-2 px-2 truncate text-sm font-medium text-center text-n-slate-12"
>
<span v-dompurify-html="fileNameFromDataUrl" class="truncate" />
</div>
<div class="flex items-center gap-2 ml-2 shrink-0">
<NextButton
v-if="isImage"
icon="i-lucide-zoom-in"
slate
ghost
@click="onZoom(0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-zoom-out"
slate
ghost
@click="onZoom(-0.1)"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-ccw"
slate
ghost
@click="onRotate('counter-clockwise')"
/>
<NextButton
v-if="isImage"
icon="i-lucide-rotate-cw"
slate
ghost
@click="onRotate('clockwise')"
/>
<NextButton
icon="i-lucide-download"
slate
ghost
:is-loading="isDownloading"
:disabled="isDownloading"
@click="onClickDownload"
/>
<NextButton icon="i-lucide-x" slate ghost @click="onClose" />
</div>
</header>
<main class="flex items-stretch flex-1 h-full overflow-hidden">
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-left"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === 0"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex - 1],
activeImageIndex - 1
)
"
/>
</div>
<video
v-if="isVideo"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
controls
playsInline
class="max-h-full max-w-full object-contain"
@click.stop
/>
<div class="flex-1 flex items-center justify-center overflow-hidden">
<div
v-if="isImage"
:style="imageWrapperStyle"
class="flex items-center justify-center origin-center"
:class="{
// Adjust dimensions when rotated 90/270 degrees to maintain visibility
// and prevent image from overflowing container in different aspect ratios
'w-[calc(100dvh-8rem)] h-[calc(100dvw-7rem)]':
activeImageRotation % 180 !== 0,
'size-full': activeImageRotation % 180 === 0,
}"
>
<img
ref="imageRef"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
:style="imageStyle"
class="max-h-full max-w-full object-contain duration-100 ease-in-out transform select-none"
@click.stop
@dblclick.stop="onDoubleClickZoomImage"
@wheel.prevent.stop="onWheelImageZoom"
@mousemove="onMouseMove"
@mouseleave="onMouseLeave"
/>
</div>
<audio
v-if="isAudio"
:key="activeAttachment.message_id"
controls
class="w-full max-w-md"
@click.stop
>
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
</audio>
</div>
<video
v-if="isVideo"
:key="activeAttachment.message_id"
:src="activeAttachment.data_url"
controls
playsInline
class="max-h-full max-w-full object-contain"
@click.stop
/>
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-right"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === allAttachments.length - 1"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex + 1],
activeImageIndex + 1
)
"
/>
</div>
</main>
<audio
v-if="isAudio"
:key="activeAttachment.message_id"
controls
class="w-full max-w-md"
@click.stop
>
<source :src="`${activeAttachment.data_url}?t=${Date.now()}`" />
</audio>
</div>
<footer
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
>
<div
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
<div class="flex items-center justify-center w-16 shrink-0">
<NextButton
v-if="hasMoreThanOneAttachment"
icon="i-lucide-chevron-right"
class="z-10"
blue
faded
lg
:disabled="activeImageIndex === allAttachments.length - 1"
@click.stop="
onClickChangeAttachment(
allAttachments[activeImageIndex + 1],
activeImageIndex + 1
)
"
/>
</div>
</main>
<footer
class="z-10 flex items-center justify-center h-12 border-t border-n-weak"
>
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
</div>
</footer>
</div>
</woot-modal>
<div
class="rounded-md flex items-center justify-center px-3 py-1 bg-n-slate-3 text-n-slate-12 text-sm font-medium"
>
{{ `${activeImageIndex + 1} / ${allAttachments.length}` }}
</div>
</footer>
</div>
</woot-modal>
</Teleport>
</template>
@@ -63,7 +63,6 @@ export default {
<label class="input-container">
<span v-if="label">{{ label }}</span>
<input
class="bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 border-slate-200 dark:border-slate-600"
:value="modelValue"
:type="type"
:placeholder="placeholder"
@@ -82,7 +81,7 @@ export default {
<style scoped lang="scss">
.help-text {
@apply mt-0.5 text-xs not-italic text-slate-600 dark:text-slate-400;
@apply mt-0.5 text-xs not-italic text-n-slate-11;
}
.message {
@@ -175,15 +175,15 @@ export default {
<template>
<div class="relative phone-input--wrap">
<div
class="flex items-center justify-start border border-solid rounded-md dark:bg-slate-900"
class="flex items-center justify-start border-none outline outline-1 rounded-lg bg-n-alpha-black2"
:class="
error
? 'border border-solid border-red-400 dark:border-red-400 mb-1'
: 'mb-4 border-slate-200 dark:border-slate-600'
? 'outline-n-ruby-8 dark:outline-n-ruby-8 hover:outline-n-ruby-9 dark:hover:outline-n-ruby-9 mb-1'
: 'mb-4 outline-n-weak dark:outline-n-weak hover:outline-n-slate-6 dark:hover:outline-n-slate-6'
"
>
<div
class="cursor-pointer py-2 pr-1.5 pl-2 rounded-tl-md rounded-bl-md flex items-center justify-center gap-1.5 bg-slate-25 dark:bg-slate-700 h-10 w-14"
class="cursor-pointer py-2 pr-1.5 pl-2 rounded-tl-lg rounded-bl-lg flex items-center justify-center gap-1.5 bg-n-solid-3 h-10 w-14"
@click.prevent="toggleCountryDropdown"
>
<h5 v-if="activeCountry" class="mb-0">
@@ -194,7 +194,7 @@ export default {
</div>
<span
v-if="activeDialCode"
class="flex py-2 pl-2 pr-0 text-base font-normal leading-normal bg-white dark:bg-slate-900 text-slate-800 dark:text-slate-100"
class="flex py-2 pl-2 pr-0 text-base font-normal leading-normal text-n-slate-12"
>
{{ activeDialCode }}
</span>
@@ -202,7 +202,7 @@ export default {
ref="phoneNumberInput"
:value="phoneNumber"
type="tel"
class="!mb-0 !rounded-tl-none !rounded-bl-none !border-0 font-normal !w-full dark:!bg-slate-900 text-base !px-1.5 placeholder:font-normal"
class="no-margin !rounded-tl-none !rounded-bl-none !outline-none !border-0 font-normal !w-full !bg-transparent text-base !px-1.5 placeholder:font-normal"
:placeholder="placeholder"
:readonly="readonly"
:style="styles"
@@ -215,20 +215,22 @@ export default {
ref="dropdown"
v-on-clickaway="onOutsideClick"
tabindex="0"
class="z-10 absolute h-60 w-[12.5rem] shadow-md overflow-y-auto top-10 rounded px-0 pt-0 pb-1 bg-white dark:bg-slate-900"
class="z-10 absolute h-60 w-[12.5rem] shadow-md overflow-y-auto top-10 rounded-lg px-0 pt-0 pb-1 bg-n-alpha-3 backdrop-blur-[100px]"
@keydown.prevent.up="moveUp"
@keydown.prevent.down="moveDown"
@keydown.prevent.enter="
onSelectCountry(filteredCountriesBySearch[selectedIndex])
"
>
<div class="sticky top-0 p-1 bg-white dark:bg-slate-900">
<div
class="sticky top-0 p-1 bg-white dark:bg-transparent backdrop-blur-[100px]"
>
<input
ref="searchbar"
v-model="searchCountry"
type="text"
:placeholder="$t('GENERAL.PHONE_INPUT.PLACEHOLDER')"
class="!h-8 !mb-0 !text-sm !border !border-solid !border-slate-2000 dark:!border-slate-6000"
class="!h-8 !mb-0 !text-sm !outline-n-brand dark:!outline-n-brand"
@input="onSearchCountry"
/>
</div>
@@ -236,10 +238,10 @@ export default {
v-for="(country, index) in filteredCountriesBySearch"
ref="dropdownItem"
:key="index"
class="flex items-center px-1 py-0 cursor-pointer h-7 hover:bg-slate-50 dark:hover:bg-slate-700"
class="flex items-center px-1 py-0 cursor-pointer h-7 hover:bg-n-alpha-1 dark:hover:bg-n-alpha-2"
:class="{
'bg-slate-50 dark:bg-slate-700': country.id === activeCountryCode,
'bg-slate-25 dark:bg-slate-800': index === selectedIndex,
'bg-n-alpha-1 dark:bg-n-alpha-2': country.id === activeCountryCode,
'bg-n-alpha-1 dark:bg-n-alpha-2': index === selectedIndex,
}"
@click="onSelectCountry(country)"
>
@@ -250,13 +252,13 @@ export default {
>
{{ country.name }}
</span>
<span class="ml-1 text-xs text-slate-300 dark:text-slate-300">{{
country.dial_code
}}</span>
<span class="ml-1 text-xs text-n-slate-11">
{{ country.dial_code }}
</span>
</div>
<div v-if="filteredCountriesBySearch.length === 0">
<span
class="flex items-center justify-center mt-4 text-sm text-slate-500 dark:text-slate-300"
class="flex items-center justify-center mt-4 text-sm text-n-slate-10"
>
{{ $t('GENERAL.PHONE_INPUT.EMPTY_STATE') }}
</span>