create-nextjs-cms 0.9.5 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/helpers/check-directory.d.ts +1 -0
  2. package/dist/helpers/check-directory.d.ts.map +1 -1
  3. package/dist/helpers/check-directory.js +98 -23
  4. package/dist/index.js +13 -8
  5. package/dist/lib/create-project.js +1 -1
  6. package/package.json +3 -3
  7. package/templates/default/app/(auth)/auth-language-provider.tsx +34 -34
  8. package/templates/default/components/AnalyticsPage.tsx +22 -6
  9. package/templates/default/components/CategorySectionSelectInput.tsx +2 -2
  10. package/templates/default/components/form/Form.tsx +12 -2
  11. package/templates/default/components/form/FormInputs.tsx +25 -16
  12. package/templates/default/components/form/inputs/DateFormInput.tsx +110 -53
  13. package/templates/default/components/form/inputs/DateRangeFormInput.tsx +175 -0
  14. package/templates/default/components/form/inputs/TagsFormInput.tsx +6 -5
  15. package/templates/default/next-env.d.ts +1 -1
  16. package/templates/default/package.json +3 -3
  17. package/templates/default/proxy.ts +2 -2
  18. package/templates/default/app/(rootLayout)/dashboard-new/page.tsx +0 -7
  19. package/templates/default/components/DashboardNewPage.tsx +0 -253
  20. package/templates/default/components/DashboardPage.tsx +0 -188
  21. package/templates/default/components/EmailCard.tsx +0 -138
  22. package/templates/default/components/EmailPasswordForm.tsx +0 -85
  23. package/templates/default/components/EmailQuotaForm.tsx +0 -73
  24. package/templates/default/components/EmailsPage.tsx +0 -49
  25. package/templates/default/components/NewEmailForm.tsx +0 -132
  26. package/templates/default/components/form/DateRangeFormInput.tsx +0 -57
@@ -2,5 +2,6 @@ export declare const resolveDirectory: (appName: string) => Promise<{
2
2
  targetDir: string;
3
3
  projectName: string;
4
4
  targetIsCwd: boolean;
5
+ targetExistedBefore: boolean;
5
6
  }>;
6
7
  //# sourceMappingURL=check-directory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"check-directory.d.ts","sourceRoot":"","sources":["../../src/helpers/check-directory.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,gBAAgB,GACzB,SAAS,MAAM,KAChB,OAAO,CAAC;IACP,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;CACvB,CAwCA,CAAA"}
1
+ {"version":3,"file":"check-directory.d.ts","sourceRoot":"","sources":["../../src/helpers/check-directory.ts"],"names":[],"mappings":"AA2HA,eAAO,MAAM,gBAAgB,GACzB,SAAS,MAAM,KAChB,OAAO,CAAC;IACP,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;IACpB,mBAAmB,EAAE,OAAO,CAAA;CAC/B,CAqCA,CAAA"}
@@ -6,6 +6,85 @@ import fs from 'fs-extra';
6
6
  import { isEmptyDir } from './utils.js';
7
7
  import * as p from '@clack/prompts';
8
8
  import chalk from 'chalk';
9
+ import ora from 'ora';
10
+ const abortInstallation = () => {
11
+ p.cancel('Aborting installation');
12
+ process.exit(1);
13
+ };
14
+ const abortDirectoryCleanup = (targetDir, error) => {
15
+ p.log.error(`Failed to empty ${targetDir === process.cwd() ? 'the current directory' : `"${targetDir}"`}.`);
16
+ p.log.message('Some files may already have been deleted.');
17
+ p.log.message(`Please review and clean this directory manually: ${targetDir}`);
18
+ p.log.message(`Reason: ${error instanceof Error ? error.message : 'Unknown error'}`);
19
+ return abortInstallation();
20
+ };
21
+ const getDirectoryEntriesToDelete = async (targetDir, keepGit) => {
22
+ const entries = await fs.readdir(targetDir);
23
+ return entries.filter((entry) => !(keepGit && entry === '.git'));
24
+ };
25
+ const emptyDirectory = async (targetDir, entries, onEntryDeleted) => {
26
+ let deletedCount = 0;
27
+ for (const entry of entries) {
28
+ await fs.remove(path.join(targetDir, entry));
29
+ deletedCount += 1;
30
+ onEntryDeleted?.(deletedCount, entries.length);
31
+ }
32
+ };
33
+ const runDirectoryCleanup = async ({ targetDir, targetIsCwd, keepGit, }) => {
34
+ const spinnerText = keepGit
35
+ ? `Emptying ${targetIsCwd ? 'the current directory' : `"${targetDir}"`} and keeping .git...`
36
+ : `Emptying ${targetIsCwd ? 'the current directory' : `"${targetDir}"`}...`;
37
+ const successMessage = keepGit
38
+ ? `Emptied ${targetIsCwd ? 'the current directory' : `"${targetDir}"`} and kept ${chalk.green('.git')}.`
39
+ : `Emptied ${targetIsCwd ? 'the current directory' : `"${targetDir}"`}.`;
40
+ const entriesToDelete = await getDirectoryEntriesToDelete(targetDir, keepGit);
41
+ const spinner = ora(entriesToDelete.length > 0 ? `${spinnerText} (0/${entriesToDelete.length})` : spinnerText).start();
42
+ try {
43
+ await emptyDirectory(targetDir, entriesToDelete, (deletedCount, totalCount) => {
44
+ spinner.text = `${spinnerText} (${deletedCount}/${totalCount})`;
45
+ });
46
+ }
47
+ catch (error) {
48
+ spinner.stop();
49
+ abortDirectoryCleanup(targetDir, error);
50
+ }
51
+ spinner.stop();
52
+ p.log.info(successMessage);
53
+ };
54
+ const promptForNonEmptyDirectoryAction = async (targetDir, targetIsCwd) => {
55
+ if (targetIsCwd) {
56
+ p.log.error('Current directory is not empty.');
57
+ p.log.message(chalk.gray('Tip: You can also specify a directory name like this: \n') +
58
+ chalk.green('pnpm create nextjs-cms ') +
59
+ chalk.italic.magenta('my-app'));
60
+ }
61
+ else {
62
+ p.log.error(`Directory "${targetDir}" is not empty.`);
63
+ }
64
+ p.log.message('Choose how you want to proceed:');
65
+ const action = await p.select({
66
+ message: 'How would you like to proceed?',
67
+ initialValue: 'abort',
68
+ options: [
69
+ { value: 'abort', label: 'Abort' },
70
+ { value: 'empty', label: 'Empty directory ⚠️', hint: 'Delete everything in the target directory' },
71
+ {
72
+ value: 'empty-keep-git',
73
+ label: 'Empty directory and keep .git folder ⚠️',
74
+ hint: 'Delete everything except the root .git folder',
75
+ },
76
+ {
77
+ value: 'continue',
78
+ label: 'Continue anyway ⚠️',
79
+ hint: 'Existing files may be overwritten and the app may be unstable',
80
+ },
81
+ ],
82
+ });
83
+ if (p.isCancel(action) || action === 'abort') {
84
+ abortInstallation();
85
+ }
86
+ return action;
87
+ };
9
88
  export const resolveDirectory = async (appName) => {
10
89
  // Resolve target path from the caller's CWD
11
90
  const rawTarget = expandHome(appName);
@@ -13,35 +92,31 @@ export const resolveDirectory = async (appName) => {
13
92
  // Derive package name from final path
14
93
  const projectName = basename(targetDir);
15
94
  const targetIsCwd = path.normalize(targetDir) === path.normalize(process.cwd());
16
- if (targetIsCwd) {
17
- // Using current directory (".")
18
- if (!(await fs.pathExists(targetDir))) {
19
- await fs.ensureDir(targetDir);
20
- }
21
- else if (!(await isEmptyDir(targetDir))) {
22
- p.log.error('Current directory is not empty. Choose an empty folder or a new directory name.');
23
- p.log.message(chalk.gray('Tip: You can also specify a directory name like this: \n') +
24
- chalk.green('pnpm create nextjs-cms ') +
25
- chalk.italic.magenta('my-app'));
26
- p.log.message(' ');
27
- process.exit(1);
28
- }
95
+ const targetExistedBefore = await fs.pathExists(targetDir);
96
+ if (!targetExistedBefore) {
97
+ await fs.ensureDir(targetDir);
29
98
  }
30
- else {
31
- if (await fs.pathExists(targetDir)) {
32
- if (!(await isEmptyDir(targetDir))) {
33
- p.log.error(`Directory "${targetDir}" is not empty.`);
34
- p.log.message('Please choose an empty directory or a different directory name.');
35
- process.exit(1);
36
- }
37
- }
38
- else {
39
- await fs.ensureDir(targetDir);
99
+ else if (!(await isEmptyDir(targetDir))) {
100
+ const action = await promptForNonEmptyDirectoryAction(targetDir, targetIsCwd);
101
+ switch (action) {
102
+ case 'empty':
103
+ await runDirectoryCleanup({ targetDir, targetIsCwd, keepGit: false });
104
+ break;
105
+ case 'empty-keep-git':
106
+ await runDirectoryCleanup({ targetDir, targetIsCwd, keepGit: true });
107
+ break;
108
+ case 'continue':
109
+ p.log.warn('Continuing in a non-empty directory. Existing files may be overwritten.');
110
+ break;
111
+ default:
112
+ action;
113
+ break;
40
114
  }
41
115
  }
42
116
  return {
43
117
  targetDir,
44
118
  projectName,
45
119
  targetIsCwd,
120
+ targetExistedBefore,
46
121
  };
47
122
  };
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ async function createNextjsCms() {
21
21
  // Run CLI to get user input
22
22
  const { appName, sectionsToAdd, git /*, databaseProvider*/ } = await runCli();
23
23
  // Resolve directory
24
- const { targetDir, projectName, targetIsCwd } = await resolveDirectory(appName);
24
+ const { targetDir, projectName, targetIsCwd, targetExistedBefore } = await resolveDirectory(appName);
25
25
  try {
26
26
  // Detect package manager
27
27
  const preferredPM = detectPackageManager();
@@ -63,14 +63,19 @@ async function createNextjsCms() {
63
63
  p.outro(chalk.red(`❌ Failed to create project: ${error instanceof Error ? error.message : 'Unknown error'}`));
64
64
  // Clean up partial installation on error
65
65
  if ((await fs.pathExists(targetDir)) && !targetIsCwd) {
66
- p.log.info('🧹 Cleaning up partial installation...');
67
- try {
68
- await fs.remove(targetDir);
69
- p.log.success('✅ Cleanup completed.');
66
+ if (targetExistedBefore) {
67
+ p.log.warn('Project creation failed in a pre-existing directory. Existing files were left in place.');
70
68
  }
71
- catch {
72
- p.log.error('⚠️ Could not clean up partial installation.');
73
- p.log.message(` Please manually remove: ${targetDir}`);
69
+ else {
70
+ p.log.info('🧹 Cleaning up partial installation...');
71
+ try {
72
+ await fs.remove(targetDir);
73
+ p.log.success('✅ Cleanup completed.');
74
+ }
75
+ catch {
76
+ p.log.error('⚠️ Could not clean up partial installation.');
77
+ p.log.message(` Please manually remove: ${targetDir}`);
78
+ }
74
79
  }
75
80
  }
76
81
  process.exit(1);
@@ -22,7 +22,7 @@ export const createProject = async ({ targetDir, sectionsToAdd, projectName, pre
22
22
  !rel.startsWith('.turbo') &&
23
23
  !rel.includes('tsconfig.tsbuildinfo'));
24
24
  },
25
- // Overwrite is safe since target is ensured empty (or new)
25
+ // Overwrite is intentional because the target is new, user-approved, or was explicitly emptied
26
26
  overwrite: true,
27
27
  errorOnExist: false,
28
28
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nextjs-cms",
3
- "version": "0.9.5",
3
+ "version": "0.9.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,8 +29,8 @@
29
29
  "tsx": "^4.20.6",
30
30
  "typescript": "^5.9.2",
31
31
  "@lzcms/eslint-config": "0.3.0",
32
- "@lzcms/prettier-config": "0.1.0",
33
- "@lzcms/tsconfig": "0.1.0"
32
+ "@lzcms/tsconfig": "0.1.0",
33
+ "@lzcms/prettier-config": "0.1.0"
34
34
  },
35
35
  "prettier": "@lzcms/prettier-config",
36
36
  "scripts": {
@@ -1,34 +1,34 @@
1
- 'use client'
2
-
3
- import { createContext, useContext, type ReactNode } from 'react'
4
-
5
- export interface AuthLanguageValue {
6
- supportedLanguages: readonly string[]
7
- fallbackLanguage: string
8
- initialLanguage: string
9
- }
10
-
11
- const AuthLanguageContext = createContext<AuthLanguageValue | null>(null)
12
-
13
- export function AuthLanguageProvider({
14
- supportedLanguages,
15
- fallbackLanguage,
16
- initialLanguage,
17
- children,
18
- }: AuthLanguageValue & { children: ReactNode }) {
19
- return (
20
- <AuthLanguageContext.Provider
21
- value={{ supportedLanguages, fallbackLanguage, initialLanguage }}
22
- >
23
- {children}
24
- </AuthLanguageContext.Provider>
25
- )
26
- }
27
-
28
- export function useAuthLanguage(): AuthLanguageValue {
29
- const ctx = useContext(AuthLanguageContext)
30
- if (!ctx) {
31
- throw new Error('useAuthLanguage must be used within AuthLanguageProvider')
32
- }
33
- return ctx
34
- }
1
+ 'use client'
2
+
3
+ import { createContext, useContext, type ReactNode } from 'react'
4
+
5
+ export interface AuthLanguageValue {
6
+ supportedLanguages: readonly string[]
7
+ fallbackLanguage: string
8
+ initialLanguage: string
9
+ }
10
+
11
+ const AuthLanguageContext = createContext<AuthLanguageValue | null>(null)
12
+
13
+ export function AuthLanguageProvider({
14
+ supportedLanguages,
15
+ fallbackLanguage,
16
+ initialLanguage,
17
+ children,
18
+ }: AuthLanguageValue & { children: ReactNode }) {
19
+ return (
20
+ <AuthLanguageContext.Provider
21
+ value={{ supportedLanguages, fallbackLanguage, initialLanguage }}
22
+ >
23
+ {children}
24
+ </AuthLanguageContext.Provider>
25
+ )
26
+ }
27
+
28
+ export function useAuthLanguage(): AuthLanguageValue {
29
+ const ctx = useContext(AuthLanguageContext)
30
+ if (!ctx) {
31
+ throw new Error('useAuthLanguage must be used within AuthLanguageProvider')
32
+ }
33
+ return ctx
34
+ }
@@ -3,7 +3,7 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
3
3
  import React, { useEffect, useRef } from 'react'
4
4
  import { useI18n } from 'nextjs-cms/translations/client'
5
5
  import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js'
6
- import { DatePickerWithRange } from '@/components/form/DateRangeFormInput'
6
+ import DateRangeFormInput from '@/components/form/inputs/DateRangeFormInput'
7
7
  import { MonthlyPageViews } from '@/components/analytics/MonthlyPageViews'
8
8
  import { TopSources } from '@/components/analytics/TopSources'
9
9
  import { TopMediums } from '@/components/analytics/TopMediums'
@@ -15,17 +15,19 @@ import { BounceRate } from '@/components/analytics/BounceRate'
15
15
  import { LivePageViews } from '@/components/analytics/LivePageViews'
16
16
  import { TopCountries } from '@/components/analytics/TopCountries'
17
17
  import { TopDevices } from '@/components/analytics/TopDevices'
18
- import { trpc } from '@/app/_trpc/client'
18
+ // import { trpc } from '@/app/_trpc/client'
19
19
  ChartJS.register(ArcElement, Tooltip, Legend)
20
20
 
21
+ // TODO: MOVE THIS TO THE PLUGIN
22
+
21
23
  const AnalyticsPage = () => {
22
24
  const t = useI18n()
23
25
  const controller = new AbortController()
24
26
  const datePickerRangeRef = useRef<HTMLDivElement>(null)
25
- const [fromDate, setFromDate] = React.useState<Date | string>('30daysAgo')
26
- const [toDate, setToDate] = React.useState<Date | string>('today')
27
+ const [fromDate, setFromDate] = React.useState<string>('30daysAgo')
28
+ const [toDate, setToDate] = React.useState<string>('today')
27
29
 
28
- const { data, isError } = trpc.googleAnalytics.test.useQuery()
30
+ // const { data, isError } = trpc.googleAnalytics.test.useQuery()
29
31
 
30
32
  useEffect(() => {
31
33
  return () => {
@@ -92,7 +94,21 @@ const AnalyticsPage = () => {
92
94
  </Tabs>
93
95
  </Tabs>
94
96
  <div ref={datePickerRangeRef} className='hidden'>
95
- <DatePickerWithRange />
97
+ <DateRangeFormInput input={{
98
+ name: 'dateRange',
99
+ label: 'Date Range',
100
+ required: true,
101
+ conditionalFields: [],
102
+ readonly: false,
103
+ defaultValue: undefined,
104
+ value: undefined,
105
+ startName: 'fromDate',
106
+ endName: 'toDate',
107
+ format: 'date',
108
+ startValue: fromDate,
109
+ endValue: toDate,
110
+ type: 'date_range',
111
+ }} />
96
112
  </div>
97
113
  </div>
98
114
  <div className='my-2 grid grid-cols-2 gap-4 sm-sidebar:grid-cols-2 md-sidebar:grid-cols-2 lg-sidebar:grid-cols-4'>
@@ -25,9 +25,9 @@ export default function CategorySectionSelectInput({
25
25
  /**
26
26
  * Check and add the select option if it does not exist, the select option has a value of undefined
27
27
  */
28
- const exists = input.options?.filter((option) => option.value === undefined)
28
+ const exists = input?.options?.filter((option) => option.value === undefined)
29
29
  if (!exists || exists.length === 0) {
30
- if (input.options) {
30
+ if (input?.options) {
31
31
  // @ts-ignore - This is a type-hack to add the placeholder `select` option to the options array
32
32
  input.options.unshift({ value: undefined, label: t('select') })
33
33
  }
@@ -16,6 +16,7 @@ import {
16
16
  CheckboxFieldClientConfig,
17
17
  ColorFieldClientConfig,
18
18
  DateFieldClientConfig,
19
+ DateRangeFieldClientConfig,
19
20
  DocumentFieldClientConfig,
20
21
  MapFieldClientConfig,
21
22
  NumberFieldClientConfig,
@@ -36,6 +37,7 @@ import {
36
37
  selectFieldSchema,
37
38
  selectMultipleFieldSchema,
38
39
  dateFieldSchema,
40
+ dateRangeFieldSchema,
39
41
  checkboxFieldSchema,
40
42
  textareaFieldSchema,
41
43
  richTextFieldSchema,
@@ -79,6 +81,8 @@ export default function Form({
79
81
  section: {
80
82
  name: string
81
83
  gallery?: boolean
84
+ title?: { section?: string; singular?: string; plural?: string }
85
+ configFile?: string
82
86
  }
83
87
  inputGroups:
84
88
  | {
@@ -152,6 +156,12 @@ export default function Form({
152
156
  })
153
157
  break
154
158
 
159
+ case 'date_range':
160
+ schema = schema.extend(
161
+ dateRangeFieldSchema(input as DateRangeFieldClientConfig, language),
162
+ )
163
+ break
164
+
155
165
  case 'checkbox':
156
166
  schema = schema.extend({
157
167
  [input.name]: checkboxFieldSchema(input as CheckboxFieldClientConfig, language),
@@ -263,7 +273,7 @@ export default function Form({
263
273
  <div className='rounded border border-amber-300 bg-amber-50 p-4 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300'>
264
274
  <h3 className='font-semibold'>{t('noLocalizedFields')}</h3>
265
275
  <p className='mt-1'>
266
- {t('noLocalizedFieldsHint', { section: data.section.title.section, file: data.section.configFile })}
276
+ {t('noLocalizedFieldsHint', { section: typeof data.section.title === 'object' ? (data.section.title?.section ?? '') : '', file: data.section.configFile })}
267
277
  </p>
268
278
  </div>
269
279
  ) : (
@@ -286,7 +296,7 @@ export default function Form({
286
296
  {data.section.gallery && !isTranslationMode ? (
287
297
  <>
288
298
  <div className='w-full'>
289
- <PhotoGallery sectionName={data.section.name} gallery={data.gallery} />
299
+ <PhotoGallery sectionName={data.section.name} gallery={'gallery' in data ? data.gallery : []} />
290
300
  </div>
291
301
  <div className='w-full'>
292
302
  <Dropzone ref={dropzoneRef} />
@@ -1,8 +1,9 @@
1
- import React from 'react'
1
+ import React from 'react'
2
2
  import ColorFormInput from '@/components/form/inputs/ColorFormInput'
3
3
  import NumberFormInput from '@/components/form/inputs/NumberFormInput'
4
4
  import TextFormInput from '@/components/form/inputs/TextFormInput'
5
5
  import DateFormInput from '@/components/form/inputs/DateFormInput'
6
+ import DateRangeFormInput from '@/components/form/inputs/DateRangeFormInput'
6
7
  import RichTextFormInput from '@/components/form/inputs/RichTextFormInput'
7
8
  import SelectFormInput from '@/components/form/inputs/SelectFormInput'
8
9
  import MultipleSelectFormInput from '@/components/form/inputs/MultipleSelectFormInput'
@@ -20,6 +21,7 @@ import {
20
21
  CheckboxFieldClientConfig,
21
22
  ColorFieldClientConfig,
22
23
  DateFieldClientConfig,
24
+ DateRangeFieldClientConfig,
23
25
  DocumentFieldClientConfig,
24
26
  MapFieldClientConfig,
25
27
  NumberFieldClientConfig,
@@ -35,15 +37,15 @@ import {
35
37
  SlugFieldClientConfig,
36
38
  } from 'nextjs-cms/core/fields'
37
39
 
38
- export default function FormInputs({
39
- inputs,
40
- sectionName,
41
- submitSuccessCount = 0,
42
- }: {
43
- inputs: FieldClientConfig[]
44
- sectionName: string
45
- submitSuccessCount?: number
46
- }) {
40
+ export default function FormInputs({
41
+ inputs,
42
+ sectionName,
43
+ submitSuccessCount = 0,
44
+ }: {
45
+ inputs: FieldClientConfig[]
46
+ sectionName: string
47
+ submitSuccessCount?: number
48
+ }) {
47
49
  return (
48
50
  <>
49
51
  {inputs?.length > 0 &&
@@ -57,6 +59,13 @@ export default function FormInputs({
57
59
  return <TextFormInput input={input as TextFieldClientConfig} key={input.name} />
58
60
  case 'date':
59
61
  return <DateFormInput input={input as DateFieldClientConfig} key={input.name} />
62
+ case 'date_range':
63
+ return (
64
+ <DateRangeFormInput
65
+ input={input as DateRangeFieldClientConfig}
66
+ key={(input as DateRangeFieldClientConfig).startName}
67
+ />
68
+ )
60
69
  case 'rich_text':
61
70
  return <RichTextFormInput input={input as RichTextFieldClientConfig} key={input.name} />
62
71
  case 'textarea':
@@ -78,12 +87,12 @@ export default function FormInputs({
78
87
  )
79
88
  case 'photo':
80
89
  return (
81
- <PhotoFormInput
82
- sectionName={sectionName}
83
- input={input as PhotoFieldClientConfig}
84
- submitSuccessCount={submitSuccessCount}
85
- key={input.name}
86
- />
90
+ <PhotoFormInput
91
+ sectionName={sectionName}
92
+ input={input as PhotoFieldClientConfig}
93
+ submitSuccessCount={submitSuccessCount}
94
+ key={input.name}
95
+ />
87
96
  )
88
97
  case 'video':
89
98
  return (