|
|
@@ -1,172 +1,200 @@
|
|
|
<template>
|
|
|
- <div class="q-pa-md">
|
|
|
-
|
|
|
- <div v-if="loading" class="flex flex-center q-py-xl">
|
|
|
- <q-spinner color="primary" size="48px" />
|
|
|
- </div>
|
|
|
-
|
|
|
- <div
|
|
|
- v-else-if="filteredItems.length === 0"
|
|
|
- class="text-center text-grey q-py-xl"
|
|
|
- >
|
|
|
- Nenhum produto encontrado
|
|
|
- </div>
|
|
|
-
|
|
|
- <div v-else class="row q-col-gutter-md items-stretch">
|
|
|
-
|
|
|
- <div
|
|
|
- v-for="item in filteredItems"
|
|
|
- :key="item.id"
|
|
|
- class="col-12 col-sm-6 col-md-4"
|
|
|
- >
|
|
|
-
|
|
|
- <q-card flat bordered class="store-card">
|
|
|
-
|
|
|
- <q-card-section class="q-pa-sm">
|
|
|
-
|
|
|
- <div class="row no-wrap store-card-inner">
|
|
|
-
|
|
|
- <!-- LEFT -->
|
|
|
- <div class="col column justify-between q-pr-sm">
|
|
|
-
|
|
|
- <div>
|
|
|
+ <div>
|
|
|
+ <DefaultHeaderPage />
|
|
|
+
|
|
|
+ <div class="q-pa-md">
|
|
|
+
|
|
|
+ <!-- Search -->
|
|
|
+ <div class="row q-col-gutter-sm q-mb-md">
|
|
|
+ <div class="col-12 col-md-5">
|
|
|
+ <q-input
|
|
|
+ v-model="search"
|
|
|
+ :placeholder="$t('associado.filter_by_category')"
|
|
|
+ outlined
|
|
|
+ dense
|
|
|
+ clearable
|
|
|
+ >
|
|
|
+ <template #prepend>
|
|
|
+ <q-icon name="mdi-magnify" />
|
|
|
+ </template>
|
|
|
+ </q-input>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <div
|
|
|
- class="text-subtitle1 text-weight-bold text-violet-medium q-mb-xs"
|
|
|
- >
|
|
|
- {{ item.name }}
|
|
|
- </div>
|
|
|
+ <!-- Category chips -->
|
|
|
+ <div class="row q-gutter-xs q-mb-md flex-wrap">
|
|
|
+ <div
|
|
|
+ :class="['cat-chip', activeTab === 'all' ? 'cat-chip--selected' : 'cat-chip--default']"
|
|
|
+ @click="activeTab = 'all'"
|
|
|
+ >
|
|
|
+ {{ $t('loja.filter_all') }}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-for="cat in storeCategories"
|
|
|
+ :key="cat.value"
|
|
|
+ :class="['cat-chip', activeTab === cat.value ? 'cat-chip--selected' : 'cat-chip--default']"
|
|
|
+ @click="activeTab = cat.value"
|
|
|
+ >
|
|
|
+ {{ cat.label }}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ :class="['cat-chip', activeTab === 'recent' ? 'cat-chip--selected' : 'cat-chip--default']"
|
|
|
+ @click="activeTab = 'recent'"
|
|
|
+ >
|
|
|
+ {{ $t('loja.filter_recent') }}
|
|
|
+ </div>
|
|
|
+ <div class="cat-chip cat-chip--disabled">
|
|
|
+ {{ $t('loja.filter_others') }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- <div
|
|
|
- v-if="item.description"
|
|
|
- class="text-caption text-grey-7 q-mb-sm ellipsis-2-lines"
|
|
|
- >
|
|
|
- {{ item.description }}
|
|
|
- </div>
|
|
|
+ <!-- Loading -->
|
|
|
+ <div v-if="loading" class="flex flex-center q-py-xl">
|
|
|
+ <q-spinner color="primary" size="48px" />
|
|
|
+ </div>
|
|
|
|
|
|
- <!-- VARIACOES -->
|
|
|
- <template v-if="item.variations?.length">
|
|
|
+ <!-- Empty -->
|
|
|
+ <div v-else-if="pagedItems.length === 0" class="text-center text-grey q-py-xl">
|
|
|
+ {{ $t('http.errors.no_records_found') }}
|
|
|
+ </div>
|
|
|
|
|
|
- <div class="text-caption text-grey-6 q-mb-xs">
|
|
|
- {{ variationTypeLabel(item) }}
|
|
|
+ <!-- Grid -->
|
|
|
+ <div v-else class="row q-col-gutter-md items-stretch">
|
|
|
+ <div
|
|
|
+ v-for="item in pagedItems"
|
|
|
+ :key="item.id"
|
|
|
+ class="col-12 col-sm-6 col-md-4"
|
|
|
+ >
|
|
|
+ <q-card flat bordered class="store-card" style="height: 100%">
|
|
|
+ <q-card-section class="q-pa-sm col column" style="height: 100%">
|
|
|
+ <div class="row no-wrap store-card-inner" style="flex: 1">
|
|
|
+
|
|
|
+ <!-- LEFT -->
|
|
|
+ <div class="col column justify-between q-pr-sm">
|
|
|
+ <div>
|
|
|
+ <!-- Category badge -->
|
|
|
+ <div class="q-mb-xs">
|
|
|
+ <span v-if="item.category?.name" class="badge-category">
|
|
|
+ {{ item.category.name }}
|
|
|
+ </span>
|
|
|
</div>
|
|
|
|
|
|
- <div class="row q-gutter-xs q-mb-sm">
|
|
|
-
|
|
|
- <div
|
|
|
- v-for="v in item.variations"
|
|
|
- :key="v.id"
|
|
|
- :class="[
|
|
|
- 'variation-tag',
|
|
|
- activeVariation(item)?.id === v.id
|
|
|
- ? 'variation-tag--selected'
|
|
|
- : 'variation-tag--default'
|
|
|
- ]"
|
|
|
- @click="selectVariation(item, v)"
|
|
|
- >
|
|
|
- {{ v.variation_label }}
|
|
|
- </div>
|
|
|
-
|
|
|
+ <div class="text-subtitle1 text-weight-bold text-violet-medium q-mb-xs leading-tight">
|
|
|
+ {{ item.name }}
|
|
|
</div>
|
|
|
|
|
|
- </template>
|
|
|
-
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- PRECO -->
|
|
|
- <div class="column items-start q-mt-sm">
|
|
|
-
|
|
|
- <span
|
|
|
- v-if="item.price"
|
|
|
- class="text-caption text-grey-5 text-strike"
|
|
|
- >
|
|
|
- R$ {{ formatPrice(item.price) }}
|
|
|
- </span>
|
|
|
+ <div v-if="item.description" class="text-caption text-grey-7 q-mb-sm ellipsis-2-lines">
|
|
|
+ {{ item.description }}
|
|
|
+ </div>
|
|
|
|
|
|
- <span
|
|
|
- class="text-subtitle1 text-weight-bold text-violet-dark"
|
|
|
- >
|
|
|
- R$ {{ formatPrice(displayPrice(item)) }}
|
|
|
- </span>
|
|
|
+ <!-- Variations -->
|
|
|
+ <template v-if="item.variations?.length">
|
|
|
+ <div class="text-caption text-grey-6 q-mb-xs">
|
|
|
+ {{ variationTypeLabel(item) }}
|
|
|
+ </div>
|
|
|
+ <div class="row q-gutter-xs q-mb-sm">
|
|
|
+ <div
|
|
|
+ v-for="v in item.variations"
|
|
|
+ :key="v.id"
|
|
|
+ :class="[
|
|
|
+ 'variation-tag',
|
|
|
+ activeVariation(item)?.id === v.id
|
|
|
+ ? 'variation-tag--selected'
|
|
|
+ : 'variation-tag--default'
|
|
|
+ ]"
|
|
|
+ @click="selectVariation(item, v)"
|
|
|
+ >
|
|
|
+ {{ v.variation_label }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
|
|
|
+ <!-- Price -->
|
|
|
+ <div class="column items-start q-mt-sm">
|
|
|
+ <span v-if="item.price" class="text-caption text-grey-5 text-strike">
|
|
|
+ R$ {{ formatPrice(item.price) }}
|
|
|
+ </span>
|
|
|
+ <span class="text-subtitle1 text-weight-bold text-violet-dark">
|
|
|
+ R$ {{ formatPrice(displayPrice(item)) }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
|
|
|
- </div>
|
|
|
-
|
|
|
- <!-- RIGHT -->
|
|
|
- <div class="store-card-right column items-stretch">
|
|
|
-
|
|
|
- <div class="store-card-image">
|
|
|
-
|
|
|
- <img
|
|
|
- v-if="item.media?.length"
|
|
|
- :src="item.media[0].url"
|
|
|
- :alt="item.name"
|
|
|
- class="store-card-img"
|
|
|
- />
|
|
|
+ <!-- RIGHT -->
|
|
|
+ <div class="store-card-right column items-stretch no-wrap">
|
|
|
+ <div class="store-card-image col">
|
|
|
+ <img
|
|
|
+ v-if="item.media?.length"
|
|
|
+ :src="item.media[0].url"
|
|
|
+ :alt="item.name"
|
|
|
+ class="store-card-img"
|
|
|
+ />
|
|
|
+ <q-icon
|
|
|
+ v-else
|
|
|
+ name="mdi-image-off-outline"
|
|
|
+ size="32px"
|
|
|
+ color="grey-4"
|
|
|
+ class="absolute-center"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
|
|
|
- <q-icon
|
|
|
- v-else
|
|
|
- name="mdi-image-off-outline"
|
|
|
- size="32px"
|
|
|
- color="grey-4"
|
|
|
- class="absolute-center"
|
|
|
+ <!-- Button -->
|
|
|
+ <q-btn
|
|
|
+ unelevated
|
|
|
+ size="sm"
|
|
|
+ class="q-mt-xs"
|
|
|
+ :class="item.user_interested ? 'btn-interested' : 'btn-gradient'"
|
|
|
+ :icon="item.user_interested ? 'mdi-check' : 'mdi-thumb-up'"
|
|
|
+ :label="item.user_interested ? $t('loja.btn_interested') : $t('loja.btn_want')"
|
|
|
+ style="width: 100%"
|
|
|
+ padding="5px 8px"
|
|
|
+ @click="toggleInterest(item)"
|
|
|
/>
|
|
|
-
|
|
|
</div>
|
|
|
|
|
|
- <!-- BOTAO -->
|
|
|
- <q-btn
|
|
|
- unelevated
|
|
|
- size="sm"
|
|
|
- class="btn-gradient q-mt-xs"
|
|
|
- :color="item.user_interested ? 'negative' : 'primary'"
|
|
|
- :icon="
|
|
|
- item.user_interested
|
|
|
- ? 'mdi-heart-remove'
|
|
|
- : 'mdi-heart-outline'
|
|
|
- "
|
|
|
- :label="
|
|
|
- item.user_interested
|
|
|
- ? 'Remover Interesse'
|
|
|
- : 'Demonstrar Interesse'
|
|
|
- "
|
|
|
- @click="toggleInterest(item)"
|
|
|
- />
|
|
|
-
|
|
|
</div>
|
|
|
+ </q-card-section>
|
|
|
+ </q-card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
|
|
|
- </div>
|
|
|
-
|
|
|
- </q-card-section>
|
|
|
-
|
|
|
- </q-card>
|
|
|
-
|
|
|
+ <!-- Pagination -->
|
|
|
+ <div v-if="totalPages > 1" class="flex flex-center q-mt-lg">
|
|
|
+ <q-pagination
|
|
|
+ v-model="currentPage"
|
|
|
+ :max="totalPages"
|
|
|
+ boundary-links
|
|
|
+ color="violet-normal"
|
|
|
+ />
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
-
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted } from "vue";
|
|
|
+import { ref, computed, onMounted, watch } from "vue";
|
|
|
import { useQuasar } from "quasar";
|
|
|
-
|
|
|
-import {
|
|
|
- getStoreItems,
|
|
|
- toggleInterest as apiToggleInterest,
|
|
|
-} from "src/api/storeItem";
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
+import { normalizeString } from "src/helpers/utils";
|
|
|
+import { getStoreItems, toggleInterest as apiToggleInterest } from "src/api/storeItem";
|
|
|
+import { getCategories } from "src/api/category";
|
|
|
+import DefaultHeaderPage from "src/components/layout/DefaultHeaderPage.vue";
|
|
|
|
|
|
const $q = useQuasar();
|
|
|
+const { t } = useI18n();
|
|
|
|
|
|
const items = ref([]);
|
|
|
const loading = ref(true);
|
|
|
+const search = ref("");
|
|
|
+const activeTab = ref("all");
|
|
|
+const storeCategories = ref([]);
|
|
|
+const currentPage = ref(1);
|
|
|
+const perPage = 12;
|
|
|
|
|
|
const selectedVariations = ref({});
|
|
|
|
|
|
-const filteredItems = computed(() => items.value);
|
|
|
-
|
|
|
const selectVariation = (item, variation) => {
|
|
|
selectedVariations.value[item.id] = variation;
|
|
|
};
|
|
|
@@ -176,67 +204,81 @@ const activeVariation = (item) =>
|
|
|
|
|
|
const displayPrice = (item) => {
|
|
|
const v = activeVariation(item);
|
|
|
-
|
|
|
- if (v?.variation_value != null) {
|
|
|
- return v.variation_value;
|
|
|
- }
|
|
|
-
|
|
|
+ if (v?.variation_value != null) return v.variation_value;
|
|
|
return item.associate_price ?? item.price;
|
|
|
};
|
|
|
|
|
|
const variationTypeLabel = (item) => {
|
|
|
const type = item.variations?.[0]?.variation_type;
|
|
|
-
|
|
|
- if (type === "tamanho") return "Tamanho";
|
|
|
- if (type === "cor") return "Cor";
|
|
|
- if (type === "modelo") return "Modelo";
|
|
|
-
|
|
|
+ if (type === "tamanho") return t("loja.variation_tamanho");
|
|
|
+ if (type === "cor") return t("loja.variation_cor");
|
|
|
+ if (type === "modelo") return t("loja.variation_modelo");
|
|
|
return "";
|
|
|
};
|
|
|
|
|
|
const formatPrice = (price) =>
|
|
|
- Number(price).toLocaleString("pt-BR", {
|
|
|
- minimumFractionDigits: 2,
|
|
|
- });
|
|
|
+ Number(price).toLocaleString("pt-BR", { minimumFractionDigits: 2 });
|
|
|
+
|
|
|
+const filteredItems = computed(() => {
|
|
|
+ let list = items.value;
|
|
|
+
|
|
|
+ if (activeTab.value === "recent") {
|
|
|
+ const cutoff = new Date().setDate(new Date().getDate() - 30);
|
|
|
+ list = list.filter((i) => new Date(i.created_at).getTime() >= cutoff);
|
|
|
+ } else if (activeTab.value !== "all") {
|
|
|
+ list = list.filter((i) => i.category_id === activeTab.value);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (search.value) {
|
|
|
+ const needle = normalizeString(search.value);
|
|
|
+ list = list.filter((i) => normalizeString(i.name || "").includes(needle));
|
|
|
+ }
|
|
|
+
|
|
|
+ return list;
|
|
|
+});
|
|
|
+
|
|
|
+const totalPages = computed(() => Math.ceil(filteredItems.value.length / perPage));
|
|
|
+
|
|
|
+const pagedItems = computed(() => {
|
|
|
+ const start = (currentPage.value - 1) * perPage;
|
|
|
+ return filteredItems.value.slice(start, start + perPage);
|
|
|
+});
|
|
|
+
|
|
|
+watch([search, activeTab], () => {
|
|
|
+ currentPage.value = 1;
|
|
|
+});
|
|
|
|
|
|
const toggleInterest = async (item) => {
|
|
|
try {
|
|
|
const result = await apiToggleInterest(item.id);
|
|
|
-
|
|
|
item.user_interested = result.interested;
|
|
|
-
|
|
|
- item.interests_count += result.interested ? 1 : -1;
|
|
|
-
|
|
|
+ item.interests_count = (item.interests_count ?? 0) + (result.interested ? 1 : -1);
|
|
|
$q.notify({
|
|
|
type: "positive",
|
|
|
message: result.interested
|
|
|
- ? "Interesse demonstrado!"
|
|
|
- : "Interesse removido!",
|
|
|
+ ? t("loja.btn_interested")
|
|
|
+ : t("http.success"),
|
|
|
});
|
|
|
-
|
|
|
} catch {
|
|
|
- $q.notify({
|
|
|
- type: "negative",
|
|
|
- message: "Erro ao processar interesse",
|
|
|
- });
|
|
|
+ $q.notify({ type: "negative", message: t("http.errors.failed") });
|
|
|
}
|
|
|
};
|
|
|
|
|
|
onMounted(async () => {
|
|
|
try {
|
|
|
- const response = await getStoreItems();
|
|
|
-
|
|
|
- items.value = response;
|
|
|
-
|
|
|
- response.forEach((item) => {
|
|
|
+ const [itemsData, catsData] = await Promise.all([
|
|
|
+ getStoreItems(),
|
|
|
+ getCategories("store"),
|
|
|
+ ]);
|
|
|
+ items.value = itemsData;
|
|
|
+ storeCategories.value = catsData.map((c) => ({ label: c.name, value: c.id }));
|
|
|
+ itemsData.forEach((item) => {
|
|
|
if (item.variations?.length) {
|
|
|
selectedVariations.value[item.id] = item.variations[0];
|
|
|
}
|
|
|
});
|
|
|
-
|
|
|
} catch (e) {
|
|
|
console.error(e);
|
|
|
-
|
|
|
} finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
@@ -245,17 +287,15 @@ onMounted(async () => {
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
.store-card {
|
|
|
- transition: 0.2s;
|
|
|
- border-radius: 12px;
|
|
|
- height: 100%;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- box-shadow: 0 4px 16px rgba(102, 29, 117, 0.12);
|
|
|
- }
|
|
|
+ transition: box-shadow 0.2s;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ &:hover { box-shadow: 0 4px 16px rgba(102, 29, 117, 0.12); }
|
|
|
}
|
|
|
|
|
|
.store-card-inner {
|
|
|
- min-height: 140px;
|
|
|
+ min-height: 0;
|
|
|
+ flex: 1;
|
|
|
}
|
|
|
|
|
|
.store-card-right {
|
|
|
@@ -266,74 +306,103 @@ onMounted(async () => {
|
|
|
|
|
|
.store-card-image {
|
|
|
position: relative;
|
|
|
- height: 110px;
|
|
|
border-radius: 8px;
|
|
|
overflow: hidden;
|
|
|
- background: #f5f0f7;
|
|
|
border: 1px solid rgba(102, 29, 117, 0.15);
|
|
|
+ background: #f5f0f7;
|
|
|
+ min-height: 110px;
|
|
|
}
|
|
|
|
|
|
.store-card-img {
|
|
|
width: 100%;
|
|
|
height: 100%;
|
|
|
object-fit: cover;
|
|
|
+ display: block;
|
|
|
}
|
|
|
|
|
|
-.variation-tag {
|
|
|
+.ellipsis-2-lines {
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
+ line-clamp: 2;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+// --- category chips ---
|
|
|
+.cat-chip {
|
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
+ height: 28px;
|
|
|
+ padding: 0 12px;
|
|
|
+ border-radius: 5px;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 500;
|
|
|
+ cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ transition: background 0.15s, color 0.15s;
|
|
|
+
|
|
|
+ &--default { background: #c9a3dc; color: #fff; }
|
|
|
+ &--selected { background: #4d1658; color: #fff; }
|
|
|
+ &--disabled {
|
|
|
+ background: #c9a3dc;
|
|
|
+ color: rgba(255, 255, 255, 0.5);
|
|
|
+ cursor: default;
|
|
|
+ pointer-events: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// --- category badge on card ---
|
|
|
+.badge-category {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #ede0f5;
|
|
|
+ color: #4d1658;
|
|
|
+ font-size: 10px;
|
|
|
+ font-weight: 500;
|
|
|
+ padding: 2px 8px;
|
|
|
+ border-radius: 20px;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
|
|
|
+// --- variation tags ---
|
|
|
+.variation-tag {
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
min-width: 28px;
|
|
|
height: 26px;
|
|
|
-
|
|
|
padding: 0 7px;
|
|
|
-
|
|
|
border-radius: 4px;
|
|
|
-
|
|
|
font-size: 11px;
|
|
|
font-weight: 500;
|
|
|
-
|
|
|
cursor: pointer;
|
|
|
+ user-select: none;
|
|
|
+ transition: background 0.15s, color 0.15s;
|
|
|
|
|
|
- transition: 0.15s;
|
|
|
-
|
|
|
- &--default {
|
|
|
- background: #ede0f5;
|
|
|
- color: #4d1658;
|
|
|
- }
|
|
|
-
|
|
|
- &--selected {
|
|
|
- background: #4d1658;
|
|
|
- color: #fff;
|
|
|
- }
|
|
|
+ &--default { background: #ede0f5; color: #4d1658; }
|
|
|
+ &--selected { background: #4d1658; color: #ede0f5; }
|
|
|
}
|
|
|
|
|
|
+// --- buttons ---
|
|
|
.btn-gradient {
|
|
|
- background: linear-gradient(
|
|
|
- 90deg,
|
|
|
- #4d1658 0%,
|
|
|
- #8b30a5 100%
|
|
|
- ) !important;
|
|
|
-
|
|
|
+ background: linear-gradient(90deg, #4d1658 0%, #8b30a5 100%) !important;
|
|
|
color: white !important;
|
|
|
-
|
|
|
border-radius: 8px !important;
|
|
|
}
|
|
|
|
|
|
-.text-violet-dark {
|
|
|
- color: #4d1658;
|
|
|
-}
|
|
|
+.btn-gradient :deep(.q-icon) { color: white !important; }
|
|
|
|
|
|
-.text-violet-medium {
|
|
|
- color: #7b2d97;
|
|
|
+.btn-interested {
|
|
|
+ background: #22c55e !important;
|
|
|
+ color: white !important;
|
|
|
+ border-radius: 8px !important;
|
|
|
}
|
|
|
|
|
|
-.ellipsis-2-lines {
|
|
|
- display: -webkit-box;
|
|
|
- -webkit-line-clamp: 2;
|
|
|
- line-clamp: 2;
|
|
|
- -webkit-box-orient: vertical;
|
|
|
- overflow: hidden;
|
|
|
-}
|
|
|
-</style>
|
|
|
+.btn-interested :deep(.q-icon) { color: white !important; }
|
|
|
+
|
|
|
+// --- text colors ---
|
|
|
+.text-violet-dark { color: #4d1658; }
|
|
|
+.text-violet-medium { color: #7b2d97; }
|
|
|
+.leading-tight { line-height: 1.25; }
|
|
|
+</style>
|