shortcut-next 0.1.0

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.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # Quickstart Next ⚑
2
+
3
+ > Scaffold modern **Next.js 15+ projects** in seconds β€” with **MUI**, **React Hook Form**, and **TanStack Query** built-in.
4
+ > Optionally add **Tailwind CSS v4** with a single command.
5
+
6
+ ---
7
+
8
+ ## πŸš€ Features
9
+
10
+ - **Next.js 15 (App Router)** β€” modern project structure, ready to go.
11
+ - **MUI (Material UI)** β€” theming, components, dark mode ready.
12
+ - **React Hook Form** β€” forms made simple, integrated with MUI inputs.
13
+ - **TanStack Query (React Query)** β€” powerful data fetching and caching.
14
+ - **Tailwind CSS v4** (optional) β€” utility-first styling, zero config.
15
+ - **TypeScript by default** β€” strict mode enabled.
16
+ - **One CLI** β€” choose your preset: **Base** (MUI stack) or **Tailwind v4**.
17
+
18
+ ---
19
+
20
+ ## πŸ“¦ Installation
21
+
22
+ You don’t need to install globally. Use `npx`:
23
+
24
+ ```bash
25
+ npx @hadi87s/quickstart-next@latest
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../src/run.mjs').catch(e => {
3
+ console.error(e);
4
+ process.exit(1);
5
+ });
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "shortcut-next",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold Next.js apps with MUI base or Tailwind v4 preset.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "shortcut-next": "bin/quickstart-next.mjs"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/hadi87s/quickstart-next.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/hadi87s/quickstart-next/issues"
15
+ },
16
+ "homepage": "https://github.com/hadi87s/quickstart-next#readme",
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "src",
23
+ "templates",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "test": "echo \"Error: no test specified\" && exit 1"
28
+ },
29
+ "keywords": [],
30
+ "author": "",
31
+ "license": "ISC",
32
+ "type": "module",
33
+ "dependencies": {
34
+ "@clack/prompts": "^0.11.0",
35
+ "cac": "^6.7.14",
36
+ "execa": "^9.6.0",
37
+ "fs-extra": "^11.3.1",
38
+ "kolorist": "^1.8.0",
39
+ "ora": "^8.2.0"
40
+ }
41
+ }
package/src/run.mjs ADDED
@@ -0,0 +1,130 @@
1
+ import { cac } from 'cac';
2
+ import * as p from '@clack/prompts';
3
+ import { cyan, green, yellow } from 'kolorist';
4
+ import path from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import fs from 'fs-extra';
7
+ import { execa } from 'execa';
8
+ import ora from 'ora';
9
+
10
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
+ const TEMPLATE_DIR = path.join(__dirname, '..', 'templates', 'base'); // single base template
12
+
13
+ async function addTailwindV4(dest) {
14
+ // 1) Merge Tailwind v4 devDeps (remove any old ones if present)
15
+ const pkgPath = path.join(dest, 'package.json');
16
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
17
+
18
+ // Ensure devDependencies object exists
19
+ pkg.devDependencies = pkg.devDependencies || {};
20
+ // Remove v3-era keys if they exist
21
+ delete pkg.devDependencies.autoprefixer;
22
+ // Add v4 deps
23
+ pkg.devDependencies.tailwindcss = '^4.0.0';
24
+ pkg.devDependencies['@tailwindcss/postcss'] = '^4.0.0';
25
+ pkg.devDependencies.postcss = '^8.4.47';
26
+
27
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
28
+
29
+ // 2) Write PostCSS config for v4
30
+ await fs.outputFile(
31
+ path.join(dest, 'postcss.config.mjs'),
32
+ `export default { plugins: { '@tailwindcss/postcss': {} } };`
33
+ );
34
+
35
+ // 3) Inject the v4 CSS entry at the TOP of globals.css
36
+ const globalsPath = path.join(dest, 'src', 'app', 'globals.css');
37
+ const css = (await fs.pathExists(globalsPath)) ? await fs.readFile(globalsPath, 'utf8') : '';
38
+ const hasImport = /@import\s+["']tailwindcss["'];?/.test(css);
39
+ const withImport = hasImport ? css : `@import "tailwindcss";\n${css}`;
40
+ await fs.outputFile(globalsPath, withImport);
41
+
42
+ // 4) Make sure there is NO tailwind.config.* (v4 is zero-config)
43
+ const tcfgTs = path.join(dest, 'tailwind.config.ts');
44
+ const tcfgJs = path.join(dest, 'tailwind.config.js');
45
+ if (await fs.pathExists(tcfgTs)) await fs.remove(tcfgTs);
46
+ if (await fs.pathExists(tcfgJs)) await fs.remove(tcfgJs);
47
+ }
48
+
49
+ async function installDeps(pm, cwd) {
50
+ const args = ['install']; // install from package.json
51
+ await execa(pm, args, { cwd, stdio: 'inherit' });
52
+ }
53
+
54
+ async function main() {
55
+ const cli = cac('quickstart-next');
56
+ cli
57
+ .option('--preset <name>', 'base | tailwind')
58
+ .option('--pm <pm>', 'npm | pnpm | yarn | bun')
59
+ .option('--no-git', 'Skip git init')
60
+ .option('--no-install', 'Skip dependency install');
61
+ const { options } = cli.parse();
62
+
63
+ p.intro(green('Create Next.js project'));
64
+
65
+ const name = await p.text({
66
+ message: 'Project name?',
67
+ placeholder: 'my-next-app',
68
+ validate: v => (!v ? 'Required' : undefined)
69
+ });
70
+
71
+ const preset = options.preset || await p.select({
72
+ message: 'Choose a preset',
73
+ options: [
74
+ { label: 'Base (MUI, RHF, React Query)', value: 'base' },
75
+ { label: 'Tailwind v4 (Base + Tailwind)', value: 'tailwind' }
76
+ ]
77
+ });
78
+
79
+ const pm = options.pm || await p.select({
80
+ message: 'Package manager?',
81
+ options: [
82
+ { label: 'pnpm', value: 'pnpm' },
83
+ { label: 'npm', value: 'npm' },
84
+ { label: 'yarn', value: 'yarn' },
85
+ { label: 'bun', value: 'bun' }
86
+ ]
87
+ });
88
+
89
+ const dest = path.resolve(process.cwd(), name);
90
+ if (await fs.pathExists(dest) && (await fs.readdir(dest)).length) {
91
+ p.cancel('Target folder is not empty.');
92
+ process.exit(1);
93
+ }
94
+
95
+ const sp = ora(`Scaffolding ${cyan(name)} with ${yellow(preset)}...`).start();
96
+ await fs.copy(TEMPLATE_DIR, dest);
97
+
98
+ // set package name
99
+ const pkgPath = path.join(dest, 'package.json');
100
+ const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
101
+ pkg.name = name;
102
+ await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2));
103
+ sp.succeed('Files ready');
104
+
105
+ // Tailwind v4 augmentation
106
+ if (preset === 'tailwind') {
107
+ const tw = ora('Adding Tailwind v4...').start();
108
+ await addTailwindV4(dest);
109
+ tw.succeed('Tailwind v4 wired');
110
+ }
111
+
112
+ if (options.git !== false) {
113
+ await execa('git', ['init'], { cwd: dest });
114
+ await execa('git', ['add', '.'], { cwd: dest });
115
+ await execa('git', ['commit', '-m', 'chore: initial commit'], { cwd: dest });
116
+ }
117
+
118
+ if (options.install !== false) {
119
+ const inst = ora('Installing dependencies...').start();
120
+ await installDeps(pm, dest);
121
+ inst.succeed('Dependencies installed');
122
+ }
123
+
124
+ p.outro(`Done! Next steps:
125
+ 1) cd ${name}
126
+ 2) ${pm} run dev
127
+ `);
128
+ }
129
+
130
+ main();
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": ["next", "prettier"]
3
+ }
@@ -0,0 +1,19 @@
1
+ // .prettierrc.js
2
+ module.exports = {
3
+ arrowParens: 'avoid',
4
+ bracketSpacing: true,
5
+ htmlWhitespaceSensitivity: 'css',
6
+ insertPragma: false,
7
+ bracketSameLine: false,
8
+ jsxSingleQuote: true,
9
+ printWidth: 120,
10
+ proseWrap: 'preserve',
11
+ quoteProps: 'as-needed',
12
+ requirePragma: false,
13
+ semi: false,
14
+ singleQuote: true,
15
+ tabWidth: 2,
16
+ trailingComma: 'none',
17
+ useTabs: false
18
+ }
19
+
@@ -0,0 +1,13 @@
1
+ {
2
+ "editor.formatOnSave": true,
3
+ "[javascript]": {
4
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
5
+ },
6
+ "[typescript]": {
7
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
8
+ },
9
+ "[json]": {
10
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
11
+ }
12
+ }
13
+
@@ -0,0 +1 @@
1
+ ## Next.js Template
Binary file
@@ -0,0 +1,2 @@
1
+ :root { --bg:#0b0b0f; --fg:#e3e3e7; }
2
+ body { background:var(--bg); color:var(--fg); }
@@ -0,0 +1,33 @@
1
+ import type { Metadata } from 'next'
2
+ import { Geist, Geist_Mono } from 'next/font/google'
3
+ import './globals.css'
4
+ import BaseProviders from '@/providers/BaseProvider'
5
+
6
+ const geistSans = Geist({
7
+ variable: '--font-geist-sans',
8
+ subsets: ['latin']
9
+ })
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: '--font-geist-mono',
13
+ subsets: ['latin']
14
+ })
15
+
16
+ export const metadata: Metadata = {
17
+ title: 'Create Next App',
18
+ description: 'Generated by create next app'
19
+ }
20
+
21
+ export default function RootLayout({
22
+ children
23
+ }: Readonly<{
24
+ children: React.ReactNode
25
+ }>) {
26
+ return (
27
+ <html lang='en'>
28
+ <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
29
+ <BaseProviders>{children}</BaseProviders>
30
+ </body>
31
+ </html>
32
+ )
33
+ }
@@ -0,0 +1,315 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import Link from 'next/link'
5
+ import { Link as MuiLink } from '@mui/material'
6
+ import {
7
+ Box,
8
+ Button,
9
+ Card,
10
+ CardContent,
11
+ Chip,
12
+ Container,
13
+ Divider,
14
+ Grid,
15
+ Stack,
16
+ Tooltip,
17
+ Typography
18
+ } from '@mui/material'
19
+ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
20
+ import OpenInNewIcon from '@mui/icons-material/OpenInNew'
21
+ import { Github, Package, LayoutDashboard, FormInput } from 'lucide-react'
22
+ import { Icon } from '@iconify/react'
23
+
24
+ const Code = ({ children }: { children: React.ReactNode }) => (
25
+ <Box
26
+ component='code'
27
+ sx={{
28
+ display: 'inline-flex',
29
+ alignItems: 'center',
30
+ gap: 1,
31
+ px: 1.25,
32
+ py: 0.75,
33
+ borderRadius: 1,
34
+ bgcolor: 'rgba(255,255,255,0.08)',
35
+ border: '1px solid rgba(255,255,255,0.12)',
36
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
37
+ fontSize: 14
38
+ }}
39
+ >
40
+ {children}
41
+ </Box>
42
+ )
43
+
44
+ export default function Page() {
45
+ const [copied, setCopied] = React.useState(false)
46
+
47
+ const handleCopy = async (text: string) => {
48
+ try {
49
+ await navigator.clipboard.writeText(text)
50
+ setCopied(true)
51
+ setTimeout(() => setCopied(false), 1200)
52
+ } catch {
53
+ // noop
54
+ }
55
+ }
56
+
57
+ return (
58
+ <Box
59
+ sx={{
60
+ minHeight: '100dvh',
61
+ position: 'relative',
62
+ overflow: 'hidden',
63
+ bgcolor: 'background.default',
64
+ color: 'text.primary'
65
+ }}
66
+ >
67
+ {/* --- Glowing blobs --- */}
68
+ <Box
69
+ aria-hidden
70
+ sx={{
71
+ pointerEvents: 'none',
72
+ position: 'absolute',
73
+ inset: 0,
74
+ '&::before, &::after': {
75
+ content: '""',
76
+ position: 'absolute',
77
+ width: 520,
78
+ height: 520,
79
+ borderRadius: '50%',
80
+ filter: 'blur(80px)',
81
+ opacity: 0.22,
82
+ transform: 'translate(-30%, -20%)',
83
+ background: 'radial-gradient(closest-side, #7C4DFF, transparent 70%)',
84
+ animation: 'float1 16s ease-in-out infinite'
85
+ },
86
+ '&::after': {
87
+ right: -120,
88
+ bottom: -120,
89
+ left: 'auto',
90
+ top: 'auto',
91
+ width: 620,
92
+ height: 620,
93
+ opacity: 0.18,
94
+ transform: 'translate(20%, 10%)',
95
+ background: 'radial-gradient(closest-side, #00E5FF, transparent 70%)',
96
+ animation: 'float2 18s ease-in-out infinite'
97
+ },
98
+ '@keyframes float1': {
99
+ '0%, 100%': { transform: 'translate(-30%, -20%) scale(1)' },
100
+ '50%': { transform: 'translate(-10%, -10%) scale(1.08)' }
101
+ },
102
+ '@keyframes float2': {
103
+ '0%, 100%': { transform: 'translate(20%, 10%) scale(1)' },
104
+ '50%': { transform: 'translate(10%, 20%) scale(0.95)' }
105
+ }
106
+ }}
107
+ />
108
+
109
+ <Container maxWidth='lg' sx={{ position: 'relative', zIndex: 1, py: { xs: 6, md: 10 } }}>
110
+ {/* Hero */}
111
+ <Stack spacing={3} alignItems='center' textAlign='center' sx={{ mb: { xs: 6, md: 10 } }}>
112
+ <Stack direction='row' spacing={1} alignItems='center'>
113
+ <LayoutDashboard size={28} style={{ verticalAlign: 'middle' }} />
114
+ <Typography variant='h4' fontWeight={800} letterSpacing={0.2} sx={{ ml: 1 }}>
115
+ Quickstart Next
116
+ </Typography>
117
+ </Stack>
118
+
119
+ <Typography variant='h6' sx={{ maxWidth: 860, opacity: 0.9 }}>
120
+ A modern Next.js boilerplate powered by <b>MUI</b> with room for <b>React Query</b>, <b>React Hook Form</b>,
121
+ and optional <b>Tailwind&nbsp;v4</b>.
122
+ </Typography>
123
+
124
+ <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} useFlexGap flexWrap='wrap'>
125
+ <Chip
126
+ icon={<Icon icon='simple-icons:mui' width={18} height={18} style={{ borderRadius: 4 }} />}
127
+ label='MUI'
128
+ color='primary'
129
+ variant='filled'
130
+ />
131
+ <Chip icon={<FormInput size={18} />} label='React Hook Form' variant='outlined' />
132
+ <Chip
133
+ icon={<Icon icon='devicon:tailwindcss' width={18} height={18} />}
134
+ label='Tailwind v4 (optional)'
135
+ variant='outlined'
136
+ />
137
+ <Chip
138
+ icon={<Icon icon='devicon:typescript' width={18} height={18} style={{ borderRadius: 4 }} />}
139
+ label='TypeScript'
140
+ variant='outlined'
141
+ />
142
+ <Chip icon={<Icon icon='devicon:nextjs' width={18} height={18} />} label='App Router' variant='outlined' />
143
+ </Stack>
144
+
145
+ <Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} sx={{ pt: 1 }}>
146
+ <Button size='large' variant='contained' component={Link} href='/sample-form'>
147
+ Open Sample Form
148
+ </Button>
149
+ <Button
150
+ size='large'
151
+ variant='outlined'
152
+ endIcon={<OpenInNewIcon />}
153
+ component={Link}
154
+ href='#'
155
+ target='_blank'
156
+ rel='noopener'
157
+ >
158
+ View Docs
159
+ </Button>
160
+ </Stack>
161
+ </Stack>
162
+
163
+ {/* Content */}
164
+ <Grid container spacing={3}>
165
+ <Grid size={{ xs: 12, md: 7 }}>
166
+ <Card
167
+ sx={{
168
+ backdropFilter: 'saturate(120%) blur(6px)',
169
+ background: 'rgba(255,255,255,0.04)',
170
+ border: '1px solid',
171
+ borderColor: 'rgba(255,255,255,0.08)'
172
+ }}
173
+ >
174
+ <CardContent>
175
+ <Typography variant='h6' fontWeight={700} gutterBottom>
176
+ What’s included
177
+ </Typography>
178
+ <Stack spacing={1.25} sx={{ opacity: 0.9 }}>
179
+ <Typography>β€’ Next.js 15 (App Router) + TypeScript</Typography>
180
+ <Typography>β€’ MUI ThemeProvider + dark-ready UI</Typography>
181
+ <Typography>
182
+ β€’ RHF starter form at <code>/sample-form</code>
183
+ </Typography>
184
+ <Typography>β€’ Easy opt-in Tailwind v4 (via CLI preset)</Typography>
185
+ </Stack>
186
+
187
+ <Divider sx={{ my: 3 }} />
188
+
189
+ <Typography variant='subtitle2' gutterBottom>
190
+ Scaffold via npx
191
+ </Typography>
192
+
193
+ <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1.5} alignItems='center'>
194
+ <Code>npx @hadi87s/quickstart-next@latest</Code>
195
+ <Tooltip title={copied ? 'Copied!' : 'Copy'}>
196
+ <Button
197
+ variant='outlined'
198
+ size='small'
199
+ startIcon={<ContentCopyIcon fontSize='small' />}
200
+ onClick={() => handleCopy('npx @hadi87s/quickstart-next@latest')}
201
+ >
202
+ {copied ? 'Copied' : 'Copy'}
203
+ </Button>
204
+ </Tooltip>
205
+ </Stack>
206
+ </CardContent>
207
+ </Card>
208
+ </Grid>
209
+
210
+ <Grid size={{ xs: 12, md: 5 }}>
211
+ <Card
212
+ sx={{
213
+ height: '100%',
214
+ backdropFilter: 'saturate(120%) blur(6px)',
215
+ background: 'rgba(255,255,255,0.04)',
216
+ border: '1px solid',
217
+ borderColor: 'rgba(255,255,255,0.08)'
218
+ }}
219
+ >
220
+ <CardContent>
221
+ <Typography variant='h6' fontWeight={700} gutterBottom>
222
+ Tech Logos
223
+ </Typography>
224
+ <Stack direction='row' spacing={2} alignItems='center' sx={{ pb: 1.5 }}>
225
+ <Tooltip title='MUI' arrow placement='top'>
226
+ <span>
227
+ <Icon icon='simple-icons:mui' width={28} height={28} style={{ borderRadius: 4 }} />
228
+ </span>
229
+ </Tooltip>
230
+ <Tooltip title='Tailwind CSS' arrow placement='top'>
231
+ <span>
232
+ <Icon icon='devicon:tailwindcss' width={28} height={28} />
233
+ </span>
234
+ </Tooltip>
235
+ <Tooltip title='React' arrow placement='top'>
236
+ <span>
237
+ <Icon icon='devicon:react' width={28} height={28} />
238
+ </span>
239
+ </Tooltip>
240
+ <Tooltip title='Next.js' arrow placement='top'>
241
+ <span>
242
+ <Icon icon='devicon:nextjs' width={28} height={28} />
243
+ </span>
244
+ </Tooltip>
245
+ <Tooltip title='React Hook Form' arrow placement='top'>
246
+ <span>
247
+ <Icon icon='simple-icons:reacthookform' width={28} height={28} />
248
+ </span>
249
+ </Tooltip>
250
+ <Tooltip title='TypeScript' arrow placement='top'>
251
+ <span>
252
+ <Icon icon='devicon:typescript' width={28} height={28} style={{ borderRadius: 4 }} />
253
+ </span>
254
+ </Tooltip>
255
+ </Stack>
256
+
257
+ <Typography variant='body2' sx={{ opacity: 0.85 }}>
258
+ This template ships with MUI by default. You can enable Tailwind v4 at scaffold time. React Query and
259
+ other integrations can be added as the stack grows.
260
+ </Typography>
261
+
262
+ <Divider sx={{ my: 2 }} />
263
+
264
+ <Stack direction='row' spacing={1.5}>
265
+ <Button
266
+ variant='outlined'
267
+ size='small'
268
+ startIcon={<Github size={18} />}
269
+ endIcon={<OpenInNewIcon />}
270
+ component={Link}
271
+ href='https://github.com/hadi87s/quickstart-next'
272
+ target='_blank'
273
+ rel='noopener'
274
+ >
275
+ GitHub
276
+ </Button>
277
+ <Button
278
+ variant='outlined'
279
+ size='small'
280
+ startIcon={<Package size={18} />}
281
+ endIcon={<OpenInNewIcon />}
282
+ component={Link}
283
+ href='https://www.npmjs.com/package/@hadi87s/quickstart-next'
284
+ target='_blank'
285
+ rel='noopener'
286
+ >
287
+ npm
288
+ </Button>
289
+ </Stack>
290
+ </CardContent>
291
+ </Card>
292
+ </Grid>
293
+ </Grid>
294
+
295
+ {/* Footer */}
296
+ <Stack alignItems='center' sx={{ mt: 8, opacity: 0.65 }}>
297
+ <Typography variant='body2'>
298
+ Built with ❀️ by{' '}
299
+ <MuiLink
300
+ href='https://github.com/hadi87s/quickstart-next'
301
+ underline='none'
302
+ color='primary'
303
+ sx={{ fontWeight: 600 }}
304
+ target='_blank'
305
+ rel='noopener'
306
+ >
307
+ Hadi
308
+ </MuiLink>{' '}
309
+ using MUI. Ready for Tailwind v4, React Query, and more.
310
+ </Typography>
311
+ </Stack>
312
+ </Container>
313
+ </Box>
314
+ )
315
+ }
@@ -0,0 +1,16 @@
1
+ import { dirname } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { FlatCompat } from "@eslint/eslintrc";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+
8
+ const compat = new FlatCompat({
9
+ baseDirectory: __dirname,
10
+ });
11
+
12
+ const eslintConfig = [
13
+ ...compat.extends("next/core-web-vitals", "next/typescript"),
14
+ ];
15
+
16
+ export default eslintConfig;
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;