Google Fonts (#525)
* Google Fonts * Google fonts improvement * font button color * Refine font selection UI and update font fetching logic - Update FontsController to fetch Google fonts sorted by popularity. - Enhance FontCard component with additional skeleton loaders for better UX during font loading. - Adjust check icon positioning in FontCard to be absolute for consistent UI. - Remove unnecessary class in GoogleFontPicker's text input. - Add border and rounded styling to the font list container in GoogleFontPicker. - Simplify computed property for enrichedFonts in GoogleFontPicker. - Implement inline font style preview in FormCustomization component. --------- Co-authored-by: Frank <csskfaves@gmail.com> Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
68
client/components/open/editors/FontCard.vue
Normal file
68
client/components/open/editors/FontCard.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col p-3 rounded-md shadow border-gray-200 border-[0.5px] justify-between w-full cursor-pointer hover:ring ring-blue-300 relative"
|
||||
:class="{'ring': isSelected }"
|
||||
@click="$emit('select-font')"
|
||||
>
|
||||
<template v-if="isVisible">
|
||||
<link
|
||||
:href="getFontUrl"
|
||||
rel="stylesheet"
|
||||
>
|
||||
<div
|
||||
class="text-lg mb-3 font-normal"
|
||||
:style="{ 'font-family': `${fontName} !important` }"
|
||||
>
|
||||
The quick brown fox jumped over the lazy dog
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-wrap gap-2 mb-3"
|
||||
>
|
||||
<USkeleton
|
||||
class="h-5 w-full"
|
||||
/>
|
||||
<USkeleton
|
||||
class="h-5 w-3/4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="text-gray-400 flex justify-between">
|
||||
<p class="text-xs">
|
||||
{{ fontName }}
|
||||
</p>
|
||||
</div>
|
||||
<Icon
|
||||
v-if="isSelected"
|
||||
name="heroicons:check-circle-16-solid"
|
||||
class="w-5 h-5 text-nt-blue absolute bottom-4 right-4"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
fontName: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select-font'])
|
||||
|
||||
const getFontUrl = computed(() => {
|
||||
const family = props.fontName.replace(/ /g, '+')
|
||||
return `https://fonts.googleapis.com/css?family=${family}:wght@400&display=swap`
|
||||
})
|
||||
</script>
|
||||
141
client/components/open/editors/GoogleFontPicker.vue
Normal file
141
client/components/open/editors/GoogleFontPicker.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<modal
|
||||
:show="show"
|
||||
:compact-header="true"
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon
|
||||
name="ci:font"
|
||||
class="w-10 h-10 text-blue"
|
||||
/>
|
||||
</template>
|
||||
<template #title>
|
||||
Google fonts
|
||||
</template>
|
||||
|
||||
<div v-if="loading">
|
||||
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<text-input
|
||||
v-model="search"
|
||||
|
||||
name="search"
|
||||
placeholder="Search fonts"
|
||||
/>
|
||||
|
||||
<div
|
||||
ref="scrollContainer"
|
||||
class="grid grid-cols-3 gap-2 p-5 mb-5 overflow-y-scroll max-h-[24rem] border rounded-md"
|
||||
>
|
||||
<FontCard
|
||||
v-for="(fontName, index) in enrichedFonts"
|
||||
:key="fontName"
|
||||
:ref="el => setFontRef(el, index)"
|
||||
:font-name="fontName"
|
||||
:is-visible="visible[index]"
|
||||
:is-selected="selectedFont === fontName"
|
||||
@select-font="selectedFont = fontName"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<UButton
|
||||
size="md"
|
||||
color="white"
|
||||
class="mr-2"
|
||||
@click="$emit('apply', null)"
|
||||
>
|
||||
Reset
|
||||
</UButton>
|
||||
<UButton
|
||||
size="md"
|
||||
:disabled="!selectedFont"
|
||||
block
|
||||
class="flex-1"
|
||||
@click="$emit('apply', selectedFont)"
|
||||
>
|
||||
Apply
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineEmits } from "vue"
|
||||
import Fuse from "fuse.js"
|
||||
import { refDebounced, useElementVisibility } from "@vueuse/core"
|
||||
import FontCard from './FontCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
font: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close','apply'])
|
||||
const loading = ref(false)
|
||||
const fonts = ref([])
|
||||
const selectedFont = ref(props.font || null)
|
||||
const search = ref("")
|
||||
const debouncedSearch = refDebounced(search, 500)
|
||||
const scrollContainer = ref(null)
|
||||
const fontRefs = new Map()
|
||||
const visible = ref([])
|
||||
|
||||
const setFontRef = (el, index) => {
|
||||
if (el) fontRefs.set(index, el)
|
||||
}
|
||||
|
||||
const initializeVisibilityTracking = async () => {
|
||||
await nextTick() // Ensure DOM has been fully updated
|
||||
fontRefs.forEach((el, index) => {
|
||||
const visibility = useElementVisibility(el, {
|
||||
root: scrollContainer.value,
|
||||
threshold: 0.1
|
||||
})
|
||||
watch(
|
||||
() => visibility.value,
|
||||
(isVisible) => {
|
||||
if (isVisible) {
|
||||
visible.value[index] = true
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const fetchFonts = async () => {
|
||||
if (props.show) {
|
||||
selectedFont.value = props.font || null
|
||||
loading.value = true
|
||||
opnFetch('/fonts/').then((data) => {
|
||||
fonts.value = data || []
|
||||
loading.value = false
|
||||
initializeVisibilityTracking()
|
||||
})
|
||||
}
|
||||
}
|
||||
watch(() => props.show, fetchFonts)
|
||||
|
||||
|
||||
const enrichedFonts = computed(() => {
|
||||
if (search.value === "" || search.value === null) {
|
||||
return fonts.value
|
||||
}
|
||||
|
||||
// Fuze search
|
||||
const fuse = new Fuse(Object.values(fonts.value))
|
||||
return fuse.search(debouncedSearch.value).map((res) => {
|
||||
return res.item
|
||||
})
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user