chore(buddybook): improvements (#106)

This commit is contained in:
Danish Arora 2024-11-04 21:57:12 +05:30 committed by GitHub
parent 7faa598ca7
commit e5c9a06368
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 201 additions and 176 deletions

View File

@ -71,13 +71,17 @@ function App() {
setIsLoadingChains(true); setIsLoadingChains(true);
const messageGenerator = getMessagesFromStore(node as LightNode); const messageGenerator = getMessagesFromStore(node as LightNode);
// Process messages as they arrive try {
for await (const message of messageGenerator) { for await (const message of messageGenerator) {
setChainsData(prevChains => { setChainsData(prevChains => {
const blockExists = prevChains.some(block => block.blockUUID === message.blockUUID); const blockExists = prevChains.some(block => block.blockUUID === message.blockUUID);
if (blockExists) return prevChains; if (blockExists) return prevChains;
return [...prevChains, message]; return [...prevChains, message];
}); });
}
} catch (error) {
console.error("Error processing message:", error);
// Continue processing other messages
} }
setWakuStatus(prev => ({ ...prev, store: 'success' })); setWakuStatus(prev => ({ ...prev, store: 'success' }));
@ -130,9 +134,9 @@ function App() {
<Header wakuStatus={wakuStatus} /> <Header wakuStatus={wakuStatus} />
<main className="container mx-auto px-4 py-4 md:py-8 max-w-7xl"> <main className="container mx-auto px-4 py-4 md:py-8 max-w-7xl">
<Routes> <Routes>
<Route path="" element={<Home />} />
<Route path="create" element={<ChainCreationForm />} /> <Route path="create" element={<ChainCreationForm />} />
<Route path="view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} isLoading={isLoadingChains} />} /> <Route path="view" element={<ChainList chainsData={chainsData} onChainUpdate={handleChainUpdate} isLoading={isLoadingChains} />} />
<Route path="" element={<Home />} />
<Route <Route
path="sign/:chainUUID/:blockUUID" path="sign/:chainUUID/:blockUUID"
element={ element={

View File

@ -145,6 +145,7 @@ const ChainCreationForm: React.FC = () => {
value={formData.title} value={formData.title}
onChange={handleInputChange} onChange={handleInputChange}
maxLength={50} maxLength={50}
className="text-base sm:text-sm"
/> />
{errors.title && <p className="text-sm text-destructive">{errors.title}</p>} {errors.title && <p className="text-sm text-destructive">{errors.title}</p>}
</div> </div>
@ -156,10 +157,11 @@ const ChainCreationForm: React.FC = () => {
value={formData.description} value={formData.description}
onChange={handleInputChange} onChange={handleInputChange}
maxLength={500} maxLength={500}
className="min-h-[100px] text-base sm:text-sm"
/> />
{errors.description && <p className="text-sm text-destructive">{errors.description}</p>} {errors.description && <p className="text-sm text-destructive">{errors.description}</p>}
</div> </div>
<Button type="submit" className="w-full">Create Chain</Button> <Button type="submit" className="w-full py-6 text-base sm:py-2 sm:text-sm">Create Chain</Button>
</form> </form>
</CardContent> </CardContent>
<Dialog open={showModal} onOpenChange={handleCloseModal}> <Dialog open={showModal} onOpenChange={handleCloseModal}>
@ -196,15 +198,15 @@ const ChainCreationForm: React.FC = () => {
<> <>
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<QRCode <QRCode
text={`${window.location.origin}/sign/${formData.uuid}/${createdBlockUUID}`} text={`${window.location.origin}${import.meta.env.BASE_URL}sign/${formData.uuid}/${createdBlockUUID}`}
width={200} width={200}
height={200} height={200}
/> />
<p className="text-sm text-center break-all"> <p className="text-sm text-center break-all">
{`${window.location.origin}/sign/${formData.uuid}/${createdBlockUUID}`} {`${window.location.origin}${import.meta.env.BASE_URL}sign/${formData.uuid}/${createdBlockUUID}`}
</p> </p>
<Button <Button
onClick={() => navigator.clipboard.writeText(`${window.location.origin}/sign/${formData.uuid}/${createdBlockUUID}`)} onClick={() => navigator.clipboard.writeText(`${window.location.origin}${import.meta.env.BASE_URL}sign/${formData.uuid}/${createdBlockUUID}`)}
variant="outline" variant="outline"
> >
Copy Link Copy Link

View File

@ -55,28 +55,32 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
const { signMessage } = useSignMessage({ const { signMessage } = useSignMessage({
mutation: { mutation: {
onMutate() {
// Reset any previous errors when starting a new signing attempt
setError(null);
setIsSigning(true);
},
async onSuccess(signature) { async onSuccess(signature) {
if (!address || !node) return; if (!address || !node) return;
// Double check signature before proceeding
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
setError('You have already signed this chain.');
setIsSigning(false);
return;
}
const newBlock: BlockPayload = {
chainUUID: block.chainUUID,
blockUUID: uuidv4(),
title: block.title,
description: block.description,
signedMessage: signature,
timestamp: Date.now(),
signatures: [{ address, signature }],
parentBlockUUID: block.blockUUID
};
try { try {
// Double check signature before proceeding
if (block.signatures.some(sig => sig.address.toLowerCase() === address.toLowerCase())) {
setError('You have already signed this chain.');
return;
}
const newBlock: BlockPayload = {
chainUUID: block.chainUUID,
blockUUID: uuidv4(),
title: block.title,
description: block.description,
signedMessage: signature,
timestamp: Date.now(),
signatures: [{ address, signature }],
parentBlockUUID: block.blockUUID
};
const wakuMessage = createMessage(newBlock); const wakuMessage = createMessage(newBlock);
const { failures, successes } = await node.lightPush.send(encoder, wakuMessage); const { failures, successes } = await node.lightPush.send(encoder, wakuMessage);
@ -89,44 +93,55 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
} catch (error) { } catch (error) {
console.error('Error creating new block:', error); console.error('Error creating new block:', error);
setError('Failed to create new block. Please try again.'); setError('Failed to create new block. Please try again.');
} finally {
setIsSigning(false);
} }
}, },
onError(error) { onError(error) {
console.error('Error signing message:', error); console.error('Error signing message:', error);
setError('Error signing message. Please try again.'); setError('Error signing message. Please try again. If using a mobile wallet, please ensure your wallet app is open.');
},
onSettled() {
setIsSigning(false); setIsSigning(false);
} }
} }
}); });
const handleSign = () => { const handleSign = async () => {
if (!ensureWalletConnected()) { try {
return; if (!address) {
// If not connected, try to connect first
const connected = await ensureWalletConnected();
if (!connected) return;
}
// Check if already signed
if (alreadySigned) {
setError('You have already signed this chain.');
return;
}
// Prepare the message
const message = `Sign Block:
Chain UUID: ${block.chainUUID}
Block UUID: ${block.blockUUID}
Title: ${block.title}
Description: ${block.description}
Timestamp: ${new Date().getTime()}
Parent Block UUID: ${block.parentBlockUUID}
Signed by: ${ensName || address}`;
// Trigger signing
signMessage({ message });
} catch (error) {
console.error('Error in sign flow:', error);
setError('Failed to initiate signing. Please try again.');
setIsSigning(false);
} }
// Add an additional check here before signing
if (alreadySigned) {
setError('You have already signed this chain.');
return;
}
setIsSigning(true);
setError(null);
const message = `Sign Block:
Chain UUID: ${block.chainUUID}
Block UUID: ${block.blockUUID}
Title: ${block.title}
Description: ${block.description}
Timestamp: ${new Date().getTime()}
Parent Block UUID: ${block.parentBlockUUID}
Signed by: ${ensName || address}`;
signMessage({ message });
}; };
return ( return (
<> <>
<Button onClick={() => setIsOpen(true)} disabled={alreadySigned}> <Button onClick={() => setIsOpen(true)} disabled={alreadySigned}>
{alreadySigned ? 'Already Signed' : 'Sign Chain'} {alreadySigned ? 'Already Signed' : !address ? 'Connect Wallet' : 'Sign Chain'}
</Button> </Button>
<Dialog open={isOpen} onOpenChange={setIsOpen}> <Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogContent className="sm:max-w-md"> <DialogContent className="sm:max-w-md">
@ -157,6 +172,8 @@ const SignChain: React.FC<SignChainProps> = ({ block, chainsData, onSuccess }) =
</> </>
) : alreadySigned ? ( ) : alreadySigned ? (
'Already Signed' 'Already Signed'
) : !address ? (
'Connect Wallet'
) : ( ) : (
'Sign' 'Sign'
)} )}

View File

@ -23,7 +23,7 @@ const ChainList: React.FC<ChainListProps> = ({ chainsData, onChainUpdate, isLoad
const childBlocks = chainsData.filter(b => b.parentBlockUUID === block.blockUUID); const childBlocks = chainsData.filter(b => b.parentBlockUUID === block.blockUUID);
const totalSignatures = block.signatures.length + childBlocks.reduce((acc, child) => acc + child.signatures.length, 0); const totalSignatures = block.signatures.length + childBlocks.reduce((acc, child) => acc + child.signatures.length, 0);
const shareUrl = `${window.location.origin}/sign/${block.chainUUID ?? block.blockUUID}/${block.blockUUID}`; const shareUrl = `${window.location.origin}${import.meta.env.BASE_URL}sign/${block.chainUUID ?? block.blockUUID}/${block.blockUUID}`;
return ( return (
<li key={`${block.blockUUID}-${depth}`} className="mb-4"> <li key={`${block.blockUUID}-${depth}`} className="mb-4">

View File

@ -56,90 +56,70 @@ const Header: React.FC<HeaderProps> = ({ wakuStatus }) => {
}; };
return ( return (
<header className="border-b"> <header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container mx-auto px-4 py-4"> <div className="container">
<div className="flex flex-col md:flex-row justify-between items-center space-y-2 md:space-y-0"> <div className="h-14">
<div className="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-4 w-full md:w-auto"> <div className="flex h-14 items-center justify-between gap-4">
<h1 className="text-xl md:text-2xl font-bold">BuddyBook</h1> <nav className="flex items-center gap-2 md:gap-4">
<nav className="w-full md:w-auto"> <Link
<ul className="flex justify-center md:justify-start space-x-4"> to=""
<li> className={`text-sm font-medium ${location.pathname === "" ? "text-foreground" : "text-muted-foreground"}`}
<Link >
to="create" Home
className={`text-sm ${location.pathname.endsWith('/create') ? 'text-primary font-semibold' : 'text-muted-foreground'}`} </Link>
> <Link
Create Chain to="create"
</Link> className={`text-sm font-medium ${location.pathname === "/create" ? "text-foreground" : "text-muted-foreground"}`}
</li> >
<li> Create
<Link </Link>
to="view" <Link
className={`text-sm ${location.pathname.endsWith('/view') ? 'text-primary font-semibold' : 'text-muted-foreground'}`} to="view"
> className={`text-sm font-medium ${location.pathname === "/view" ? "text-foreground" : "text-muted-foreground"}`}
View Chains >
</Link> View
</li> </Link>
<li>
<Link
to="telemetry"
className={`text-sm ${location.pathname.endsWith('/telemetry') ? 'text-primary font-semibold' : 'text-muted-foreground'}`}
>
Telemetry
</Link>
</li>
</ul>
</nav> </nav>
</div>
<div className="flex items-center gap-2 md:gap-4">
<div className="flex flex-wrap justify-center md:justify-end items-center gap-2 w-full md:w-auto"> <div className="hidden md:flex items-center gap-2">
<div className="flex items-center space-x-2 text-xs md:text-sm"> {!isWakuLoading && !wakuError && (
{isWakuLoading ? ( <>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-1">
<Loader2 className="h-4 w-4 animate-spin" /> <span className="text-muted-foreground">Filter:</span>
<span className="text-muted-foreground">Connecting...</span> <div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div>
</div> </div>
) : wakuError ? ( <div className="flex items-center space-x-1">
<span className="text-destructive">Network Error</span> <span className="text-muted-foreground">Store:</span>
) : ( <div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
<> </div>
<div className="flex items-center space-x-1"> </>
<span className="text-muted-foreground">Filter:</span> )}
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.filter)}`}></div> </div>
</div>
<div className="flex items-center space-x-1">
<span className="text-muted-foreground">Store:</span>
<div className={`w-2 h-2 md:w-3 md:h-3 rounded-full ${getStatusColor(wakuStatus.store)}`}></div>
</div>
<span className="text-xs md:text-sm text-muted-foreground hidden md:inline">
{connections > 0 ? `${connections} peer${connections === 1 ? '' : 's'}` : 'Connecting...'}
</span>
</>
)}
</div>
<div className="flex items-center space-x-2">
{isWakuLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : wakuError ? (
<span className="text-xs md:text-sm text-red-500">Waku Error</span>
) : (
<span className="text-xs md:text-sm text-muted-foreground hidden md:inline">
Waku Connections: {connections}
</span>
)}
{isConnected ? ( <div className="flex items-center gap-2">
<div className="flex items-center space-x-2"> {isWakuLoading ? (
<span className="text-xs md:text-sm text-muted-foreground truncate max-w-[120px] md:max-w-none"> <Loader2 className="h-4 w-4 animate-spin" />
{ensName || (address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '')} ) : wakuError ? (
</span> <span className="text-xs text-red-500">Error</span>
<Button variant="outline" size="sm" onClick={() => disconnect()}> ) : (
Logout <div className={`w-2 h-2 rounded-full ${connections > 0 ? "bg-green-500" : "bg-yellow-500"}`} />
</Button> )}
</div>
) : ( {isConnected ? (
<ConnectKitButton /> <div className="flex items-center gap-2">
)} <span className="text-xs md:text-sm text-muted-foreground truncate max-w-[80px] md:max-w-[120px]">
{ensName || (address ? `${address.slice(0, 4)}...${address.slice(-4)}` : '')}
</span>
<Button variant="outline" size="sm" onClick={() => disconnect()}>
<span className="md:hidden">×</span>
<span className="hidden md:inline">Logout</span>
</Button>
</div>
) : (
<ConnectKitButton />
)}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@ interface QRCodeProps {
const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => { const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const isMobile = window.innerWidth < 640;
const handleCopy = async () => { const handleCopy = async () => {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
@ -20,18 +21,22 @@ const QRCode: React.FC<QRCodeProps> = ({ text, width = 256, height = 256 }) => {
return ( return (
<div className="flex flex-col items-center space-y-4"> <div className="flex flex-col items-center space-y-4">
<QRCodeSVG value={text} size={Math.min(width, height)} /> <QRCodeSVG
<div className="flex items-center space-x-2"> value={text}
size={isMobile ? Math.min(width * 0.8, window.innerWidth - 64) : Math.min(width, height)}
/>
<div className="flex items-center space-x-2 w-full max-w-[300px]">
<input <input
type="text" type="text"
value={text} value={text}
readOnly readOnly
className="flex-1 px-3 py-2 text-sm border rounded-md bg-muted" className="flex-1 px-3 py-2 text-xs sm:text-sm border rounded-md bg-muted truncate"
/> />
<Button <Button
variant="outline" variant="outline"
size="icon" size="icon"
onClick={handleCopy} onClick={handleCopy}
className="shrink-0"
> >
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />} {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button> </Button>

View File

@ -45,7 +45,11 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
const Comp = asChild ? Slot : "button" const Comp = asChild ? Slot : "button"
return ( return (
<Comp <Comp
className={cn(buttonVariants({ variant, size, className }))} className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
"min-h-[44px] px-4 py-2 md:min-h-[36px] md:px-3 md:py-1.5",
buttonVariants({ variant, size, className })
)}
ref={ref} ref={ref}
{...props} {...props}
/> />

View File

@ -9,7 +9,7 @@ const Card = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm p-4 md:p-6", "rounded-lg border bg-card text-card-foreground shadow-sm p-4 md:p-6 w-full max-w-[95vw] mx-auto",
className className
)} )}
{...props} {...props}

View File

@ -30,24 +30,16 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPortal> <DialogPrimitive.Content
<DialogOverlay /> ref={ref}
<DialogPrimitive.Content className={cn(
ref={ref} "fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
className={cn( "max-h-[85vh] overflow-y-auto",
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg", className
className )}
)} {...props}
{...props} />
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)) ))
DialogContent.displayName = DialogPrimitive.Content.displayName DialogContent.displayName = DialogPrimitive.Content.displayName

View File

@ -10,7 +10,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input <input
type={type} type={type}
className={cn( className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base md:text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"touch-manipulation min-h-[44px] md:min-h-[36px]",
className className
)} )}
ref={ref} ref={ref}

View File

@ -9,7 +9,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return ( return (
<textarea <textarea
className={cn( className={cn(
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50", "flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base md:text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
"touch-manipulation resize-y",
className className
)} )}
ref={ref} ref={ref}

View File

@ -4,14 +4,21 @@ export function useWalletPrompt() {
const { isConnected } = useAccount() const { isConnected } = useAccount()
const { connect, connectors } = useConnect() const { connect, connectors } = useConnect()
const ensureWalletConnected = () => { const ensureWalletConnected = async () => {
if (!isConnected) { if (!isConnected) {
// Find the first available connector (usually injected/metamask) try {
const connector = connectors[0] // Find the first available connector (usually injected/metamask)
if (connector) { const connector = connectors[0]
connect({ connector }) if (connector) {
await connect({ connector })
}
// Wait a brief moment for the connection to be established
await new Promise(resolve => setTimeout(resolve, 1000));
return true
} catch (error) {
console.error('Error connecting wallet:', error)
return false
} }
return false
} }
return true return true
} }

View File

@ -1,6 +1,7 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root {
--background: 0 0% 100%; --background: 0 0% 100%;
@ -27,8 +28,9 @@
--chart-3: 197 37% 24%; --chart-3: 197 37% 24%;
--chart-4: 43 74% 66%; --chart-4: 43 74% 66%;
--chart-5: 27 87% 67%; --chart-5: 27 87% 67%;
--radius: 0.5rem --radius: 0.5rem;
} }
.dark { .dark {
--background: 0 0% 3.9%; --background: 0 0% 3.9%;
--foreground: 0 0% 98%; --foreground: 0 0% 98%;
@ -53,36 +55,46 @@
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;
--chart-4: 280 65% 60%; --chart-4: 280 65% 60%;
--chart-5: 340 75% 55% --chart-5: 340 75% 55%;
} }
}
@layer base {
* { * {
@apply border-border; @apply border-border;
} }
html {
-webkit-tap-highlight-color: transparent;
}
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
} overscroll-behavior-y: none;
}
button, a {
@apply cursor-pointer touch-manipulation;
}
.container { .container {
@apply px-4 md:px-6 lg:px-8; @apply px-4 md:px-6 lg:px-8;
} }
h1 { h1 {
@apply text-2xl md:text-4xl font-bold; @apply text-2xl md:text-4xl font-bold;
} }
h2 { h2 {
@apply text-xl md:text-3xl font-semibold; @apply text-xl md:text-3xl font-semibold;
} }
h3 { h3 {
@apply text-lg md:text-2xl font-semibold; @apply text-lg md:text-2xl font-semibold;
} }
.section { .section {
@apply py-4 md:py-6 lg:py-8; @apply py-4 md:py-6 lg:py-8;
} }
.card { .card {
@apply p-4 md:p-6; @apply p-4 md:p-6;
} }