Skip to content

Commit 1ac71ec

Browse files
chiragchhatralamadassdevJhumanJ
authored
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>
1 parent 2a3aad6 commit 1ac71ec

File tree

13 files changed

+351
-19
lines changed

13 files changed

+351
-19
lines changed

.env.example

+3-1
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,6 @@ CADDY_AUTHORIZED_IPS=
8383

8484
GOOGLE_CLIENT_ID=
8585
GOOGLE_CLIENT_SECRET=
86-
GOOGLE_REDIRECT_URL=http://localhost:3000/settings/connections/callback/google
86+
GOOGLE_REDIRECT_URL=http://localhost:3000/settings/connections/callback/google
87+
88+
GOOGLE_FONTS_API_KEY=
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace App\Http\Controllers;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Facades\Http;
7+
8+
class FontsController extends Controller
9+
{
10+
public function index(Request $request)
11+
{
12+
return \Cache::remember('google_fonts', 60 * 60, function () {
13+
$url = "https://www.googleapis.com/webfonts/v1/webfonts?sort=popularity&key=" . config('services.google_fonts_api_key');
14+
$response = Http::get($url);
15+
if ($response->successful()) {
16+
$fonts = collect($response->json()['items'])->filter(function ($font) {
17+
return !in_array($font['category'], ['monospace']);
18+
})->map(function ($font) {
19+
return $font['family'];
20+
})->toArray();
21+
return response()->json($fonts);
22+
}
23+
24+
return [];
25+
});
26+
}
27+
}

app/Http/Requests/UserFormRequest.php

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function rules()
2929
'visibility' => ['required', Rule::in(Form::VISIBILITY)],
3030

3131
// Customization
32+
'font_family' => 'string|nullable',
3233
'theme' => ['required', Rule::in(Form::THEMES)],
3334
'width' => ['required', Rule::in(Form::WIDTHS)],
3435
'size' => ['required', Rule::in(Form::SIZES)],

app/Models/Forms/Form.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ class Form extends Model implements CachableAttributes
3131

3232
public const DARK_MODE_VALUES = ['auto', 'light', 'dark'];
3333

34-
public const SIZES = ['sm','md','lg'];
34+
public const SIZES = ['sm', 'md', 'lg'];
3535

36-
public const BORDER_RADIUS = ['none','small','full'];
36+
public const BORDER_RADIUS = ['none', 'small', 'full'];
3737

3838
public const THEMES = ['default', 'simple', 'notion'];
3939

@@ -53,6 +53,7 @@ class Form extends Model implements CachableAttributes
5353
'visibility',
5454

5555
// Customization
56+
'font_family',
5657
'custom_domain',
5758
'size',
5859
'border_radius',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<template>
2+
<div
3+
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"
4+
:class="{'ring': isSelected }"
5+
@click="$emit('select-font')"
6+
>
7+
<template v-if="isVisible">
8+
<link
9+
:href="getFontUrl"
10+
rel="stylesheet"
11+
>
12+
<div
13+
class="text-lg mb-3 font-normal"
14+
:style="{ 'font-family': `${fontName} !important` }"
15+
>
16+
The quick brown fox jumped over the lazy dog
17+
</div>
18+
</template>
19+
<div
20+
v-else
21+
class="flex flex-wrap gap-2 mb-3"
22+
>
23+
<USkeleton
24+
class="h-5 w-full"
25+
/>
26+
<USkeleton
27+
class="h-5 w-3/4"
28+
/>
29+
</div>
30+
31+
<div class="text-gray-400 flex justify-between">
32+
<p class="text-xs">
33+
{{ fontName }}
34+
</p>
35+
</div>
36+
<Icon
37+
v-if="isSelected"
38+
name="heroicons:check-circle-16-solid"
39+
class="w-5 h-5 text-nt-blue absolute bottom-4 right-4"
40+
/>
41+
</div>
42+
</template>
43+
44+
<script setup>
45+
import { defineEmits } from "vue"
46+
47+
const props = defineProps({
48+
fontName: {
49+
type: String,
50+
required: true
51+
},
52+
isSelected: {
53+
type: Boolean,
54+
default: false
55+
},
56+
isVisible: {
57+
type: Boolean,
58+
default: false
59+
}
60+
})
61+
62+
const emit = defineEmits(['select-font'])
63+
64+
const getFontUrl = computed(() => {
65+
const family = props.fontName.replace(/ /g, '+')
66+
return `https://fonts.googleapis.com/css?family=${family}:wght@400&display=swap`
67+
})
68+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<template>
2+
<modal
3+
:show="show"
4+
:compact-header="true"
5+
@close="$emit('close')"
6+
>
7+
<template #icon>
8+
<Icon
9+
name="ci:font"
10+
class="w-10 h-10 text-blue"
11+
/>
12+
</template>
13+
<template #title>
14+
Google fonts
15+
</template>
16+
17+
<div v-if="loading">
18+
<Loader class="h-6 w-6 text-nt-blue mx-auto" />
19+
</div>
20+
<div v-else>
21+
<text-input
22+
v-model="search"
23+
24+
name="search"
25+
placeholder="Search fonts"
26+
/>
27+
28+
<div
29+
ref="scrollContainer"
30+
class="grid grid-cols-3 gap-2 p-5 mb-5 overflow-y-scroll max-h-[24rem] border rounded-md"
31+
>
32+
<FontCard
33+
v-for="(fontName, index) in enrichedFonts"
34+
:key="fontName"
35+
:ref="el => setFontRef(el, index)"
36+
:font-name="fontName"
37+
:is-visible="visible[index]"
38+
:is-selected="selectedFont === fontName"
39+
@select-font="selectedFont = fontName"
40+
/>
41+
</div>
42+
43+
<div class="flex">
44+
<UButton
45+
size="md"
46+
color="white"
47+
class="mr-2"
48+
@click="$emit('apply', null)"
49+
>
50+
Reset
51+
</UButton>
52+
<UButton
53+
size="md"
54+
:disabled="!selectedFont"
55+
block
56+
class="flex-1"
57+
@click="$emit('apply', selectedFont)"
58+
>
59+
Apply
60+
</UButton>
61+
</div>
62+
</div>
63+
</modal>
64+
</template>
65+
66+
<script setup>
67+
import { defineEmits } from "vue"
68+
import Fuse from "fuse.js"
69+
import { refDebounced, useElementVisibility } from "@vueuse/core"
70+
import FontCard from './FontCard.vue'
71+
72+
const props = defineProps({
73+
show: {
74+
type: Boolean,
75+
default: false,
76+
},
77+
font: {
78+
type: String,
79+
default: null,
80+
},
81+
})
82+
83+
const emit = defineEmits(['close','apply'])
84+
const loading = ref(false)
85+
const fonts = ref([])
86+
const selectedFont = ref(props.font || null)
87+
const search = ref("")
88+
const debouncedSearch = refDebounced(search, 500)
89+
const scrollContainer = ref(null)
90+
const fontRefs = new Map()
91+
const visible = ref([])
92+
93+
const setFontRef = (el, index) => {
94+
if (el) fontRefs.set(index, el)
95+
}
96+
97+
const initializeVisibilityTracking = async () => {
98+
await nextTick() // Ensure DOM has been fully updated
99+
fontRefs.forEach((el, index) => {
100+
const visibility = useElementVisibility(el, {
101+
root: scrollContainer.value,
102+
threshold: 0.1
103+
})
104+
watch(
105+
() => visibility.value,
106+
(isVisible) => {
107+
if (isVisible) {
108+
visible.value[index] = true
109+
}
110+
},
111+
{ immediate: true }
112+
)
113+
})
114+
}
115+
116+
const fetchFonts = async () => {
117+
if (props.show) {
118+
selectedFont.value = props.font || null
119+
loading.value = true
120+
opnFetch('/fonts/').then((data) => {
121+
fonts.value = data || []
122+
loading.value = false
123+
initializeVisibilityTracking()
124+
})
125+
}
126+
}
127+
watch(() => props.show, fetchFonts)
128+
129+
130+
const enrichedFonts = computed(() => {
131+
if (search.value === "" || search.value === null) {
132+
return fonts.value
133+
}
134+
135+
// Fuze search
136+
const fuse = new Fuse(Object.values(fonts.value))
137+
return fuse.search(debouncedSearch.value).map((res) => {
138+
return res.item
139+
})
140+
})
141+
</script>

client/components/open/forms/OpenCompleteForm.vue

+15
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
<div
33
v-if="form"
44
class="open-complete-form"
5+
:style="{ '--font-family': form.font_family }"
56
>
7+
<link
8+
v-if="adminPreview && form.font_family"
9+
rel="stylesheet"
10+
:href="getFontUrl"
11+
>
12+
613
<h1
714
v-if="!isHideTitle"
815
class="mb-4 px-2"
@@ -249,6 +256,11 @@ export default {
249256
},
250257
isHideTitle () {
251258
return this.form.hide_title || (import.meta.client && window.location.href.includes('hide_title=true'))
259+
},
260+
getFontUrl() {
261+
if(!this.form || !this.form.font_family) return null
262+
const family = this.form?.font_family.replace(/ /g, '+')
263+
return `https://fonts.googleapis.com/css?family=${family}:wght@400,500,700,800,900&display=swap`
252264
}
253265
},
254266
@@ -330,6 +342,9 @@ export default {
330342
331343
<style lang="scss">
332344
.open-complete-form {
345+
* {
346+
font-family: var(--font-family) !important;
347+
}
333348
.form-description, .nf-text {
334349
ol {
335350
@apply list-decimal list-inside;

client/components/open/forms/components/form-components/FormCustomization.vue

+25
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,24 @@
5353
label="Form Theme"
5454
/>
5555

56+
<label class="text-gray-700 font-medium text-sm">Font Style</label>
57+
<v-button
58+
color="white"
59+
class="w-full mb-4"
60+
size="small"
61+
@click="showGoogleFontPicker = true"
62+
>
63+
<span :style="{ 'font-family': (form.font_family?form.font_family+' !important':null) }">
64+
{{ form.font_family || 'Default' }}
65+
</span>
66+
</v-button>
67+
<GoogleFontPicker
68+
:show="showGoogleFontPicker"
69+
:font="form.font_family || null"
70+
@close="showGoogleFontPicker=false"
71+
@apply="onApplyFont"
72+
/>
73+
5674
<div class="flex space-x-4 justify-stretch">
5775
<select-input
5876
name="size"
@@ -173,12 +191,14 @@
173191
<script setup>
174192
import { useWorkingFormStore } from "../../../../../stores/working_form"
175193
import EditorOptionsPanel from "../../../editors/EditorOptionsPanel.vue"
194+
import GoogleFontPicker from "../../../editors/GoogleFontPicker.vue"
176195
import ProTag from "~/components/global/ProTag.vue"
177196
178197
const workingFormStore = useWorkingFormStore()
179198
const form = storeToRefs(workingFormStore).content
180199
const isMounted = ref(false)
181200
const confetti = useConfetti()
201+
const showGoogleFontPicker = ref(false)
182202
183203
onMounted(() => {
184204
isMounted.value = true
@@ -190,4 +210,9 @@ const onChangeConfettiOnSubmission = (val) => {
190210
confetti.play()
191211
}
192212
}
213+
214+
const onApplyFont = (val) => {
215+
form.value.font_family = val
216+
showGoogleFontPicker.value = false
217+
}
193218
</script>

client/composables/forms/initForm.js

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const initForm = (defaultValue = {}, withDefaultProperties = false) => {
99
properties: withDefaultProperties ? getDefaultProperties() : [],
1010

1111
// Customization
12+
font_family: null,
1213
theme: "default",
1314
width: "centered",
1415
dark_mode: "auto",

0 commit comments

Comments
 (0)