PluginProbe ʕ •ᴥ•ʔ
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI / 3.5.2
Everest Forms – Contact Form, Payment Form, Quiz, Survey & Custom Form Builder with AI v3.5.2
3.5.2 3.5.1 3.5.0 3.4.8 3.4.7 3.4.6 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.5.1 1.1.6 1.1.7 1.1.8 1.1.9 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.10 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.6.1 1.6.7 1.7.0 1.7.0.1 1.7.0.2 1.7.0.3 1.7.1 1.7.2 1.7.2.1 1.7.2.2 1.7.3 1.7.4 1.7.5 1.7.5.1 1.7.5.2 1.7.6 1.7.7 1.7.7.1 1.7.7.2 1.7.8 1.7.9 1.8.0 1.8.0.1 1.8.1 1.8.2 1.8.2.1 1.8.2.2 1.8.2.3 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.0.1 1.9.1 1.9.2 1.9.3 1.9.4 1.9.4.1 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.0.1 2.0.1 2.0.2 2.0.3 2.0.3.1 2.0.4 2.0.4.1 2.0.5 2.0.6 2.0.7 2.0.8 2.0.8.1 2.0.9 3.0.0 3.0.0.1 3.0.1 3.0.2 3.0.3 3.0.3.1 3.0.4 3.0.4.1 3.0.4.2 3.0.5 3.0.5.1 3.0.5.2 3.0.6 3.0.6.1 3.0.7.1 3.0.8 3.0.8.1 3.0.9 3.0.9.1 3.0.9.2 3.0.9.3 3.0.9.4 3.0.9.5 3.1.0 3.1.1 3.1.2 3.2.0 3.2.1 3.2.2 3.2.3 3.2.4 3.2.5 3.2.6 3.3.0 3.4.0 3.4.1 3.4.2 3.4.2.1 3.4.3 3.4.4 3.4.5 trunk 1.0 1.0.1 1.0.2 1.0.3
everest-forms / src / templates / components / Main.tsx
everest-forms / src / templates / components Last commit date
CreateFormCTA.tsx 2 weeks ago CreateWithAI.tsx 2 weeks ago Main.tsx 2 weeks ago PluginStatus.tsx 2 weeks ago Sidebar.tsx 2 weeks ago TemplateList.tsx 2 weeks ago TemplatesSkeleton.tsx 4 months ago
Main.tsx
602 lines
1 import {
2 Box,
3 Flex,
4 Heading,
5 Icon,
6 Input,
7 InputGroup,
8 InputLeftElement,
9 keyframes,
10 Tab,
11 TabList,
12 Tabs,
13 Text,
14 useToast,
15 } from '@chakra-ui/react';
16 import { useQuery } from '@tanstack/react-query';
17 import apiFetch from '@wordpress/api-fetch';
18 import { __ } from '@wordpress/i18n';
19 import debounce from 'lodash.debounce';
20 import React, { useCallback, useEffect, useMemo, useState } from 'react';
21 import { IoSearchOutline } from 'react-icons/io5';
22 import { FiRefreshCw } from 'react-icons/fi';
23 import { templatesScriptData } from '../utils/global';
24 import Sidebar from './Sidebar';
25 import TemplateList from './TemplateList';
26 import CreateFormCTA from './CreateFormCTA';
27
28 const { restURL, security } = templatesScriptData;
29
30 const fetchTemplates = async () => {
31 const response = (await apiFetch({
32 path: `${restURL}everest-forms/v1/templates`,
33 method: 'GET',
34 headers: {
35 'X-WP-Nonce': security,
36 },
37 })) as { templates: { category: string; templates: Template[] }[] };
38
39 if (response && Array.isArray(response.templates)) {
40 const allTemplates = response.templates.flatMap(
41 (category) => category.templates,
42 );
43 return allTemplates;
44 } else {
45 throw new Error(__('Unexpected response format.', 'everest-forms'));
46 }
47 };
48
49 interface CreateFormResponse {
50 success: boolean;
51 data?: { id: number; redirect: string; status: number };
52 message?: string;
53 }
54
55 const shimmer = keyframes`
56 0% { background-position: -600px 0; }
57 100% { background-position: 600px 0; }
58 `;
59
60 const spin = keyframes`
61 from { transform: rotate(0deg); }
62 to { transform: rotate(360deg); }
63 `;
64
65 const skimmerStyle = {
66 background: 'linear-gradient(90deg, #f0f0f0 25%, #e8e8e8 50%, #f0f0f0 75%)',
67 backgroundSize: '600px 100%',
68 animation: `${shimmer} 1.6s ease-in-out infinite`,
69 borderRadius: '4px',
70 };
71
72 const SkelBox: React.FC<{ w?: string; h?: string; mb?: string; br?: string }> = ({ w = '100%', h = '14px', mb = '0', br = '4px' }) => (
73 <Box w={w} h={h} mb={mb} borderRadius={br} sx={skimmerStyle} />
74 );
75
76 // Template card skeleton — matches actual card: gradient image bg + white inner wrapper + info section
77 const TemplateCardSkeleton = () => (
78 <Box bg="white" borderRadius="12px" border="1px solid #e2e8f0" overflow="hidden" display="flex" flexDirection="column">
79 {/* Image area: gradient bg + white inner card (matching actual card structure) */}
80 <Box
81 position="relative"
82 borderBottom="1px solid #e2e8f0"
83 pt="20px" px="20px" pb="0"
84 overflow="hidden"
85 background="linear-gradient(129deg, #F3F2F8 2.83%, #F7F5F9 110.96%)"
86 minH="160px"
87 >
88 <Box
89 w="100%"
90 borderRadius="8px 8px 0 0"
91 border="1px solid #e2e8f0"
92 borderBottom="none"
93 overflow="hidden"
94 h="140px"
95 sx={skimmerStyle}
96 opacity={0.6}
97 />
98 </Box>
99 {/* Info section */}
100 <Box p="20px" flex="1">
101 <SkelBox w="62%" h="13px" mb="6px" />
102 <SkelBox w="90%" h="11px" mb="4px" />
103 <SkelBox w="76%" h="11px" />
104 </Box>
105 </Box>
106 );
107
108 const TemplateSkeleton = () => (
109 <Box p="32px">
110 {/* CTA cards — 2-col equal-width grid, matching CreateFormCTA layout */}
111 <Flex gap="32px" mb="32px" direction={{ base: 'column', md: 'row' }}>
112 {/* AI card: icon + badge row, then title/desc/link */}
113 <Box flex="1" bg="white" borderRadius="16px" p="32px" border="1px solid #e2e8f0" display="flex" flexDirection="column">
114 <Flex justify="space-between" align="center" mb="16px">
115 <Box borderRadius="12px" w="48px" h="48px" sx={skimmerStyle} />
116 <Box borderRadius="6px" w="44px" h="22px" sx={skimmerStyle} />
117 </Flex>
118 <SkelBox w="58%" h="17px" mb="8px" />
119 <SkelBox w="92%" h="12px" mb="4px" />
120 <SkelBox w="76%" h="12px" mb="24px" />
121 <SkelBox w="86px" h="13px" />
122 </Box>
123 {/* Scratch card: standalone icon, then title/desc/link */}
124 <Box flex="1" bg="white" borderRadius="16px" p="32px" border="1px solid #e2e8f0" display="flex" flexDirection="column">
125 <Box borderRadius="12px" w="48px" h="48px" mb="20px" sx={skimmerStyle} />
126 <SkelBox w="62%" h="17px" mb="8px" />
127 <SkelBox w="92%" h="12px" mb="4px" />
128 <SkelBox w="80%" h="12px" mb="24px" />
129 <SkelBox w="70px" h="13px" />
130 </Box>
131 </Flex>
132
133 {/* Template section card */}
134 <Box bg="white" borderRadius="16px" border="1px solid #e2e8f0" overflow="hidden">
135
136 {/* Top bar: search (left, w-256px) | heading + filter tabs (right) */}
137 <Flex borderBottom="1px solid #e2e8f0" direction={{ base: 'column', md: 'row' }}>
138 <Box w={{ base: '100%', md: '256px' }} minW={{ md: '256px' }} p="20px" borderRight={{ base: 'none', md: '1px solid #e2e8f0' }}>
139 {/* Search input shimmer */}
140 <Box h="36px" borderRadius="8px" sx={skimmerStyle} />
141 </Box>
142 <Flex flex="1" px="28px" py="20px" align="center" justify="space-between">
143 {/* "Choose from Templates" heading */}
144 <SkelBox w="190px" h="19px" />
145 {/* Filter tabs pill */}
146 <Box borderRadius="8px" w="150px" h="34px" sx={skimmerStyle} />
147 </Flex>
148 </Flex>
149
150 {/* Sidebar + template grid */}
151 <Flex direction={{ base: 'column', md: 'row' }}>
152 {/* Sidebar: CATEGORIES label + rows + Can't find card */}
153 <Box w={{ base: '100%', md: '256px' }} minW={{ md: '256px' }} p="20px" pt="12px" borderRight={{ base: 'none', md: '1px solid #e2e8f0' }}>
154 {/* "CATEGORIES" label */}
155 <Box w="72px" h="10px" mb="10px" sx={skimmerStyle} borderRadius="3px" />
156 {/* Category rows */}
157 {[78, 62, 88, 55, 72, 60, 70, 52, 65, 58].map((w, i) => (
158 <Flex key={i} justify="space-between" align="center" mb="2px" px="12px" py="12px" borderRadius="8px">
159 <Box w={`${w}%`} h="13px" sx={skimmerStyle} borderRadius="3px" />
160 <Box w="20px" h="13px" sx={skimmerStyle} borderRadius="3px" />
161 </Flex>
162 ))}
163 {/* "Can't find a template?" card */}
164 <Box mt="20px" borderRadius="12px" border="1px solid #e2e8f0" p="16px">
165 <SkelBox w="68%" h="13px" mb="6px" />
166 <SkelBox w="92%" h="11px" mb="4px" />
167 <SkelBox w="80%" h="11px" mb="12px" />
168 <Box h="36px" borderRadius="8px" sx={skimmerStyle} />
169 </Box>
170 </Box>
171
172 {/* Template grid: 2-col (xl: 3-col) matching TemplateList */}
173 <Box p={{ base: '20px', md: '28px' }} pt="12px" flex={1} minW="0">
174 <Box sx={{
175 display: 'grid',
176 gridTemplateColumns: 'repeat(2, 1fr)',
177 gap: '16px',
178 '@media (min-width: 1280px)': { gridTemplateColumns: 'repeat(3, 1fr)' },
179 }}>
180 {Array.from({ length: 6 }).map((_, i) => (
181 <TemplateCardSkeleton key={i} />
182 ))}
183 </Box>
184 </Box>
185 </Flex>
186 </Box>
187 </Box>
188 );
189
190 const Main: React.FC<{ onCreateWithAI?: (formId?: number, title?: string) => void }> = ({ onCreateWithAI }) => {
191 const toast = useToast();
192 const [filter, setFilter] = useState(__('All', 'everest-forms'));
193 const [isCreatingBlank, setIsCreatingBlank] = useState(false);
194 const [searchInputValue, setSearchInputValue] = useState('');
195 const [state, setState] = useState({
196 selectedCategory: __('All Forms', 'everest-forms'),
197 searchTerm: '',
198 });
199 const [categorySetFromURL, setCategorySetFromURL] = useState(false);
200
201 const { selectedCategory, searchTerm } = state;
202
203 const {
204 data: templates = [],
205 isLoading,
206 isFetching,
207 refetch,
208 error,
209 } = useQuery(['templates'], fetchTemplates);
210
211 const categories = useMemo(() => {
212 const categoriesSet = new Set<string>();
213 templates.forEach((template) => {
214 template.categories.forEach((category) => categoriesSet.add(category));
215 });
216
217 return [
218 { name: __('All Forms', 'everest-forms'), count: templates.length },
219 ...Array.from(categoriesSet).map((category) => ({
220 name: category,
221 count: templates.filter((template) =>
222 template.categories.includes(category),
223 ).length,
224 })),
225 ];
226 }, [templates]);
227
228 useEffect(() => {
229 if (categorySetFromURL) return;
230 if (categories.length <= 1) return;
231
232 const urlParams = new URLSearchParams(window.location.search);
233
234 if (urlParams.has('evf_template_category')) {
235 const categorySlug = urlParams.get('evf_template_category') || '';
236
237 const normalize = (str: string) =>
238 str
239 .toLowerCase()
240 .replace(/\s+/g, '')
241 .replace(/[^a-z0-9]/g, '');
242
243 const normalizedSlug = normalize(categorySlug);
244
245 let matchedCategory = categories.find((cat) => {
246 const normalizedCatName = normalize(cat.name);
247
248 if (normalizedCatName === normalizedSlug) return true;
249 if (normalizedCatName.startsWith(normalizedSlug)) return true;
250 if (normalizedSlug.startsWith(normalizedCatName)) return true;
251
252 return false;
253 });
254
255 if (!matchedCategory) {
256 matchedCategory = categories.find((cat) => {
257 const normalizedCatName = normalize(cat.name);
258
259 const slugWords = categorySlug.toLowerCase().split(/[\s-]+/);
260 const catWords = cat.name.toLowerCase().split(/[\s-]+/);
261
262 const hasMatchingWord = slugWords.some((word) =>
263 catWords.some(
264 (catWord) => catWord.includes(word) || word.includes(catWord),
265 ),
266 );
267
268 if (hasMatchingWord) return true;
269
270 if (
271 normalizedCatName.includes(normalizedSlug) ||
272 normalizedSlug.includes(normalizedCatName)
273 ) return true;
274
275 return false;
276 });
277 }
278
279 if (
280 matchedCategory &&
281 matchedCategory.name !== __('All Forms', 'everest-forms')
282 ) {
283 setState((prevState) => ({
284 ...prevState,
285 selectedCategory: matchedCategory.name,
286 }));
287 setCategorySetFromURL(true);
288 }
289 }
290 }, [categories, categorySetFromURL]);
291
292 const filteredTemplates = useMemo(() => {
293 return templates.filter(
294 (template) =>
295 template.slug !== 'blank' &&
296 (selectedCategory === __('All Forms', 'everest-forms') ||
297 template.categories.includes(selectedCategory)) &&
298 template.title.toLowerCase().includes(searchTerm.toLowerCase()) &&
299 (filter === __('All', 'everest-forms') ||
300 (filter === __('Free', 'everest-forms') && !template.isPro) ||
301 (filter === __('Premium', 'everest-forms') && template.isPro)),
302 );
303 }, [selectedCategory, searchTerm, templates, filter]);
304
305 const handleCategorySelect = useCallback((category: string) => {
306 setState((prevState) => ({ ...prevState, selectedCategory: category }));
307 }, []);
308
309 const debouncedSetSearch = useCallback(
310 debounce((value: string) => {
311 setState((prevState) => ({ ...prevState, searchTerm: value }));
312 }, 300),
313 [],
314 );
315
316 const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
317 const value = e.target.value;
318 setSearchInputValue(value);
319 debouncedSetSearch(value);
320 };
321
322 if (isLoading) return <TemplateSkeleton />;
323 if (error) return <div>{(error as Error).message}</div>;
324
325 const handleCreateWithAI = (formId?: number, title?: string) => {
326 if (onCreateWithAI) onCreateWithAI(formId, title);
327 };
328
329 const handleCreateBlank = async () => {
330 setIsCreatingBlank(true);
331 try {
332 const response = (await apiFetch({
333 path: `${restURL}everest-forms/v1/templates/create`,
334 method: 'POST',
335 body: JSON.stringify({
336 title: __('Untitled', 'everest-forms'),
337 slug: 'blank',
338 }),
339 headers: {
340 'Content-Type': 'application/json',
341 'X-WP-Nonce': security,
342 },
343 })) as CreateFormResponse;
344
345 if (response.success && response.data) {
346 window.location.href = response.data.redirect;
347 } else {
348 setIsCreatingBlank(false);
349 toast({
350 title: __('Error', 'everest-forms'),
351 description: response.message || __('Failed to create form.', 'everest-forms'),
352 status: 'error',
353 position: 'bottom-right',
354 duration: 5000,
355 isClosable: true,
356 variant: 'subtle',
357 });
358 }
359 } catch (error) {
360 setIsCreatingBlank(false);
361 toast({
362 title: __('Error', 'everest-forms'),
363 description: __('An error occurred while creating the form.', 'everest-forms'),
364 status: 'error',
365 position: 'bottom-right',
366 duration: 5000,
367 isClosable: true,
368 variant: 'subtle',
369 });
370 }
371 };
372
373 const filterLabels = [
374 __('All', 'everest-forms'),
375 __('Free', 'everest-forms'),
376 __('Premium', 'everest-forms'),
377 ];
378
379 return (
380 <Box p="32px">
381 {/* CTA Cards — 2-column equal-width grid */}
382 <Box mb="32px">
383 <CreateFormCTA
384 onCreateWithAI={handleCreateWithAI}
385 onCreateBlank={handleCreateBlank}
386 isCreatingBlank={isCreatingBlank}
387 />
388 </Box>
389
390 {/* Template Section Card */}
391 <Box
392 bg="white"
393 borderRadius="16px"
394 border="1px solid #e2e8f0"
395 overflow="hidden"
396 position="relative"
397 >
398 {/* Subtle refetch indicator — thin animated bar at top */}
399 {isFetching && !isLoading && (
400 <Box
401 position="absolute"
402 top="0"
403 left="0"
404 right="0"
405 h="2px"
406 zIndex={10}
407 borderRadius="16px 16px 0 0"
408 overflow="hidden"
409 >
410 <Box
411 position="absolute"
412 top="0"
413 left="0"
414 right="0"
415 h="100%"
416 bg="rgba(117,69,187,0.15)"
417 />
418 <Box
419 position="absolute"
420 top="0"
421 h="100%"
422 w="40%"
423 bg="#7545BB"
424 sx={{
425 animation: 'refetch-slide 1s ease-in-out infinite',
426 '@keyframes refetch-slide': {
427 '0%': { left: '-40%' },
428 '100%': { left: '140%' },
429 },
430 }}
431 />
432 </Box>
433 )}
434
435 {/* Top bar: search (left) | heading + filter tabs (right) */}
436 <Flex
437 align="center"
438 borderBottom="1px solid #e2e8f0"
439 direction={{ base: 'column', md: 'row' }}
440 >
441 {/* Search area — aligned with sidebar width */}
442 <Box
443 w={{ base: '100%', md: '256px' }}
444 minW={{ md: '256px' }}
445 p="20px"
446 borderRight={{ base: 'none', md: '1px solid #e2e8f0' }}
447 borderBottom={{ base: '1px solid #e2e8f0', md: 'none' }}
448 >
449 <InputGroup>
450 <InputLeftElement pointerEvents="none" h="36px">
451 <Icon as={IoSearchOutline} boxSize="4" color="#999" />
452 </InputLeftElement>
453 <Input
454 placeholder={__('Search templates', 'everest-forms')}
455 value={searchInputValue}
456 onChange={handleSearchInputChange}
457 fontSize="14px"
458 border="1px solid #e2e8f0"
459 borderRadius="8px"
460 h="36px"
461 pl="36px"
462 pr="12px"
463 _focus={{
464 borderColor: '#7545BB',
465 outline: 'none',
466 boxShadow: 'none',
467 }}
468 _placeholder={{ color: '#999' }}
469 />
470 </InputGroup>
471 </Box>
472
473 {/* Heading + filter tabs */}
474 <Flex
475 flex="1"
476 align="center"
477 justify="space-between"
478 px={{ base: '20px', md: '28px' }}
479 py="20px"
480 wrap="wrap"
481 gap="12px"
482 >
483 <Heading
484 as="h2"
485 fontSize="20px"
486 fontWeight="500"
487 color="#0e0e0e"
488 m="0"
489 letterSpacing="-0.01em"
490 >
491 {__('Choose from Templates', 'everest-forms')}
492 </Heading>
493
494 {/* Refetch button + Filter tabs */}
495 <Flex align="center" gap="10px">
496 <Box
497 as="button"
498 display="inline-flex"
499 alignItems="center"
500 gap="6px"
501 px="10px"
502 py="6px"
503 borderRadius="8px"
504 border="1px solid #e2e8f0"
505 bg="white"
506 fontSize="12px"
507 fontWeight="500"
508 color="#6b6b6b"
509 cursor={isFetching ? 'not-allowed' : 'pointer'}
510 onClick={() => { if (!isFetching) refetch(); }}
511 _hover={{ borderColor: '#7545BB', color: '#7545BB' }}
512 transition="all 0.15s"
513 title={__('Refresh templates', 'everest-forms')}
514 >
515 <Icon
516 as={FiRefreshCw}
517 boxSize="12px"
518 sx={isFetching ? { animation: `${spin} 0.7s linear infinite` } : {}}
519 />
520 {__('Refetch', 'everest-forms')}
521 </Box>
522
523 <Tabs
524 variant="unstyled"
525 onChange={(index) => setFilter(filterLabels[index])}
526 >
527 <TabList
528 bg="#f1f5f9"
529 border="1px solid #e2e8f0"
530 borderRadius="8px"
531 p="4px"
532 gap="0"
533 >
534 {filterLabels.map((label) => (
535 <Tab
536 key={label}
537 px="12px"
538 py="6px"
539 borderRadius="6px"
540 fontSize="12px"
541 fontWeight="500"
542 color="#6b6b6b"
543 _selected={{
544 bg: 'white',
545 color: '#7445ba',
546 boxShadow: '0 1px 3px rgba(0,0,0,0.08)',
547 }}
548 _hover={{ color: '#0e0e0e' }}
549 transition="all 0.15s"
550 >
551 {label}
552 </Tab>
553 ))}
554 </TabList>
555 </Tabs>
556 </Flex>
557 </Flex>
558 </Flex>
559
560 {/* Sidebar + Template Grid */}
561 <Flex direction={{ base: 'column', md: 'row' }}>
562 {/* Sidebar */}
563 <Box
564 w={{ base: '100%', md: '256px' }}
565 minW={{ md: '256px' }}
566 p="20px"
567 pt="12px"
568 borderRight={{ base: 'none', md: '1px solid #e2e8f0' }}
569 borderBottom={{ base: '1px solid #e2e8f0', md: 'none' }}
570 >
571 <Sidebar
572 categories={categories}
573 selectedCategory={state.selectedCategory}
574 onCategorySelect={handleCategorySelect}
575 onRequestTemplate={handleCreateWithAI}
576 />
577 </Box>
578
579 {/* Template grid */}
580 <Box
581 p={{ base: '20px', md: '28px' }}
582 pt="12px"
583 flex={1}
584 minW="0"
585 opacity={isFetching && !isLoading ? 0.55 : 1}
586 transition="opacity 0.25s ease"
587 pointerEvents={isFetching && !isLoading ? 'none' : 'auto'}
588 >
589 <TemplateList
590 selectedCategory={selectedCategory}
591 templates={filteredTemplates}
592 onCreateWithAI={handleCreateWithAI}
593 />
594 </Box>
595 </Flex>
596 </Box>
597 </Box>
598 );
599 };
600
601 export default Main;
602