create-bawo-frontend
Version:
Scaffold React (Vite) or Next.js with Tailwind, Zustand, Prettier, optional shadcn/ui, Framer Motion, and GSAP.
325 lines (301 loc) • 13 kB
JavaScript
// utils.ts for shadcn projects
const CN_UTIL_TS = `
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
`.trim();
const COMPONENTS_JSON_VITE = `
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": { "config": "tailwind.config.cjs", "css": "src/styles/index.css", "baseColor": "slate", "cssVariables": true },
"aliases": { "components": "@/components", "utils": "@/lib/utils" }
}
`.trim();
const COMPONENTS_JSON_NEXT = `
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": { "config": "tailwind.config.cjs", "css": "app/globals.css", "baseColor": "slate", "cssVariables": true },
"aliases": { "components": "@/components", "utils": "@/lib/utils" }
}
`.trim();
const FRAMER_DEMO_REACT = `
import { motion } from "framer-motion";
export default function FramerDemo() {
return (
<div className="mt-6 flex gap-4">
<motion.div initial={{ y: -20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ type: "spring", stiffness: 120 }} className="h-12 w-12 rounded-lg bg-blue-500" />
<motion.button whileTap={{ scale: 0.95 }} whileHover={{ scale: 1.03 }} className="rounded border px-3 py-1">Framer Motion</motion.button>
</div>
);
}
`.trim();
const GSAP_DEMO_REACT_TS = `
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
export default function GsapDemo() {
const boxRef = useRef<HTMLDivElement>(null);
useEffect(() => { if (boxRef.current) { gsap.from(boxRef.current, { y: -30, opacity: 0, duration: 0.6, ease: "power2.out" }); } }, []);
return <div className="mt-6"><div ref={boxRef} className="h-12 w-12 rounded-lg bg-emerald-500" /></div>;
}
`.trim();
const GSAP_DEMO_REACT_JS = `
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
export default function GsapDemo() {
const boxRef = useRef(null);
useEffect(() => { if (boxRef.current) { gsap.from(boxRef.current, { y: -30, opacity: 0, duration: 0.6, ease: "power2.out" }); } }, []);
return <div className="mt-6"><div ref={boxRef} className="h-12 w-12 rounded-lg bg-emerald-500" /></div>;
}
`.trim();
const FRAMER_DEMO_NEXT = `
"use client";
import { motion } from "framer-motion";
export default function FramerDemo() {
return (
<div className="mt-6 flex gap-4">
<motion.div initial={{ y: -20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ type: "spring", stiffness: 120 }} className="h-12 w-12 rounded-lg bg-blue-500" />
<motion.button whileTap={{ scale: 0.95 }} whileHover={{ scale: 1.03 }} className="rounded border px-3 py-1">Framer Motion</motion.button>
</div>
);
}
`.trim();
const GSAP_DEMO_NEXT = `
"use client";
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
export default function GsapDemo() {
const boxRef = useRef<HTMLDivElement>(null);
useEffect(() => { if (boxRef.current) { gsap.from(boxRef.current, { y: -30, opacity: 0, duration: 0.6, ease: "power2.out" }); } }, []);
return <div className="mt-6"><div ref={boxRef} className="h-12 w-12 rounded-lg bg-emerald-500" /></div>;
}
`.trim();
const GSAP_DEMO_NEXT_JS = `
"use client";
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
export default function GsapDemo() {
const boxRef = useRef(null);
useEffect(() => {
if (boxRef.current) {
gsap.from(boxRef.current, { y: -30, opacity: 0, duration: 0.6, ease: "power2.out" });
}
}, []);
return <div className="mt-6"><div ref={boxRef} className="h-12 w-12 rounded-lg bg-emerald-500" /></div>;
}
`.trim();
// Redux demos & store pieces
const REDUX_DEMO_JS = `
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement, reset } from '../../store/counterSlice';
export default function ReduxDemo() {
const count = useSelector((state) => state.counter.value);
const dispatch = useDispatch();
return (
<div className="mt-6 p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-3">Redux Counter</h3>
<div className="flex items-center gap-4">
<span className="text-xl font-mono">{count}</span>
<button onClick={() => dispatch(increment())} className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600">+</button>
<button onClick={() => dispatch(decrement())} className="px-3 py-1 bg-red-500 text-white rounded hover:bg-red-600">-</button>
<button onClick={() => dispatch(reset())} className="px-3 py-1 bg-gray-500 text-white rounded hover:bg-gray-600">Reset</button>
</div>
</div>
);
}
`.trim();
const RTK_QUERY_DEMO_JS = `
import React from 'react';
import { useGetPostsQuery, useGetPostQuery } from '../../store/api';
export default function RTKQueryDemo() {
const { data: posts, error, isLoading } = useGetPostsQuery();
const { data: post } = useGetPostQuery(1);
if (isLoading) return <div className="mt-6">Loading posts...</div>;
if (error) return <div className="mt-6 text-red-500">Error loading posts</div>;
return (
<div className="mt-6 p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-3">RTK Query Demo</h3>
<div className="space-y-2">
<div className="text-sm text-gray-600">First post: {post?.title}</div>
<div className="text-sm">Total posts: {posts?.length}</div>
</div>
</div>
);
}
`.trim();
const REACT_QUERY_DEMO_JS = `
import React from 'react';
import { useQuery } from '@tanstack/react-query';
const fetchPosts = async () => (await fetch('https://jsonplaceholder.typicode.com/posts?_limit=5')).json();
export default function ReactQueryDemo() {
const { data: posts, isLoading, error } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
if (isLoading) return <div className="mt-6">Loading posts...</div>;
if (error) return <div className="mt-6 text-red-500">Error: {error.message}</div>;
return (
<div className="mt-6 p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-3">React Query Demo</h3>
<div className="space-y-2">
{posts?.slice(0, 3).map((post) => <div key={post.id} className="text-sm p-2 bg-gray-50 rounded">{post.title}</div>)}
</div>
</div>
);
}
`.trim();
const SWR_DEMO_JS = `
import React from 'react';
import useSWR from 'swr';
const fetcher = (url) => fetch(url).then((r) => r.json());
export default function SWRDemo() {
const { data, error, isLoading } = useSWR('https://jsonplaceholder.typicode.com/users?_limit=3', fetcher);
if (error) return <div className="mt-6 text-red-500">Failed to load</div>;
if (isLoading) return <div className="mt-6">Loading users...</div>;
return (
<div className="mt-6 p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-3">SWR Demo</h3>
<div className="space-y-2">
{data?.map((u) => <div key={u.id} className="text-sm p-2 bg-gray-50 rounded">{u.name} - {u.email}</div>)}
</div>
</div>
);
}
`.trim();
const CONTEXT_DEMO_JS = `
import React, { createContext, useContext, useReducer } from 'react';
const ThemeContext = createContext();
const themeReducer = (state, action) => {
switch (action.type) {
case 'TOGGLE_THEME': return { ...state, theme: state.theme === 'light' ? 'dark' : 'light' };
case 'SET_USER': return { ...state, user: action.payload };
default: return state;
}
};
export const ThemeProvider = ({ children }) => {
const [state, dispatch] = useReducer(themeReducer, { theme: 'light', user: null });
return <ThemeContext.Provider value={{ state, dispatch }}>{children}</ThemeContext.Provider>;
};
export default function ContextDemo() {
const { state, dispatch } = useContext(ThemeContext);
return (
<div className="mt-6 p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-3">Context API + useReducer Demo</h3>
<div className="space-y-3">
<div className="text-sm">Current theme: <span className="font-mono">{state.theme}</span></div>
<button onClick={() => dispatch({ type: 'TOGGLE_THEME' })} className="px-3 py-1 bg-purple-500 text-white rounded hover:bg-purple-600">Toggle Theme</button>
</div>
</div>
);
}
`.trim();
const ROUTER_DEMO_JS = `
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useNavigate } from 'react-router-dom';
const Home = () => (<div className="p-4"><h2 className="text-xl font-semibold">Home Page</h2><p className="mt-2 text-gray-600">Welcome to the home page!</p></div>);
const About = () => (<div className="p-4"><h2 className="text-xl font-semibold">About Page</h2><p className="mt-2 text-gray-600">This is the about page.</p></div>);
const Navigation = () => { const navigate = useNavigate(); return (
<nav className="flex gap-4 p-4 bg-gray-100 rounded">
<Link to="/" className="text-blue-500 hover:text-blue-700">Home</Link>
<Link to="/about" className="text-blue-500 hover:text-blue-700">About</Link>
<button onClick={() => navigate('/about')} className="px-2 py-1 text-xs bg-blue-500 text-white rounded">Navigate to About</button>
</nav>); };
export default function RouterDemo() {
return (
<div className="mt-6 p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-3">React Router Demo</h3>
<Router>
<Navigation />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
</div>
);
}
`.trim();
const USESTATE_DEMO_JS = `
import React, { useState, useEffect } from 'react';
export default function UseStateDemo() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [todos, setTodos] = useState([]);
useEffect(() => { setTodos([{ id: 1, text: 'Learn React', completed: false }, { id: 2, text: 'Build awesome apps', completed: true }]); }, []);
const addTodo = () => { if (name.trim()) { setTodos([...todos, { id: Date.now(), text: name, completed: false }]); setName(''); } };
return (
<div className="mt-6 p-4 border rounded-lg">
<h3 className="text-lg font-semibold mb-3">useState + useEffect Demo</h3>
<div className="space-y-4">
<div className="flex items-center gap-4"><span>Counter: {count}</span><button onClick={() => setCount(count + 1)} className="px-2 py-1 bg-green-500 text-white rounded text-sm">+1</button></div>
<div className="flex gap-2"><input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Add todo..." className="px-2 py-1 border rounded flex-1" /><button onClick={addTodo} className="px-3 py-1 bg-blue-500 text-white rounded text-sm">Add</button></div>
<div className="space-y-1">{todos.map((t) => <div key={t.id} className="text-sm p-2 bg-gray-50 rounded">{t.text} {t.completed && '✓'}</div>)}</div>
</div>
</div>
);
}
`.trim();
const REDUX_STORE_JS = `
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
`.trim();
const REDUX_STORE_JS_TAIL = `
export const store = configureStore({
reducer: {
counter: counterReducer,{{API_REDUCER}}
},{{API_MIDDLEWARE}}
});
export default store;
`.trim();
const COUNTER_SLICE_JS = `
import { createSlice } from '@reduxjs/toolkit';
const initialState = { value: 0 };
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => { state.value += 1; },
decrement: (state) => { state.value -= 1; },
reset: (state) => { state.value = 0; },
},
});
export const { increment, decrement, reset } = counterSlice.actions;
export default counterSlice.reducer;
`.trim();
const RTK_API_JS = `
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: 'https://jsonplaceholder.typicode.com/' }),
endpoints: (b) => ({
getPosts: b.query({ query: () => 'posts?_limit=5' }),
getPost: b.query({ query: (id) => \`posts/\${id}\` }),
}),
});
export const { useGetPostsQuery, useGetPostQuery } = api;
`.trim();
module.exports = {
CN_UTIL_TS,
COMPONENTS_JSON_VITE,
COMPONENTS_JSON_NEXT,
FRAMER_DEMO_REACT,
GSAP_DEMO_REACT_TS,
GSAP_DEMO_REACT_JS,
FRAMER_DEMO_NEXT,
GSAP_DEMO_NEXT,
GSAP_DEMO_NEXT_JS,
REDUX_DEMO_JS,
RTK_QUERY_DEMO_JS,
REACT_QUERY_DEMO_JS,
SWR_DEMO_JS,
CONTEXT_DEMO_JS,
ROUTER_DEMO_JS,
USESTATE_DEMO_JS,
REDUX_STORE_JS,
REDUX_STORE_JS_TAIL,
COUNTER_SLICE_JS,
RTK_API_JS,
};