365 lines
12 KiB
TypeScript
365 lines
12 KiB
TypeScript
/**
|
||
* Cloudflare KV Demo Component
|
||
*
|
||
* This component demonstrates how to use the Cloudflare KV utilities
|
||
* in a React application with proper loading states and error handling.
|
||
*/
|
||
|
||
import React, { useState } from 'react'
|
||
import { useCloudflareKV, useKVValue } from '../hooks/useCloudflareKV'
|
||
|
||
interface DemoData {
|
||
message: string
|
||
timestamp: string
|
||
counter: number
|
||
}
|
||
|
||
export const CloudflareKVDemo: React.FC = () => {
|
||
const [inputKey, setInputKey] = useState('demo:test')
|
||
const [inputValue, setInputValue] = useState('Hello, Cloudflare KV!')
|
||
const [searchPrefix, setSearchPrefix] = useState('demo:')
|
||
const [logs, setLogs] = useState<string[]>([])
|
||
|
||
// Add log function
|
||
const addLog = (message: string) => {
|
||
const timestamp = new Date().toLocaleTimeString()
|
||
const logMessage = `[${timestamp}] ${message}`
|
||
console.log(logMessage)
|
||
setLogs(prev => [logMessage, ...prev].slice(0, 20)) // Keep last 20 logs
|
||
}
|
||
|
||
// Use the general KV hook
|
||
const kv = useCloudflareKV()
|
||
|
||
// Use the specific value hook for a counter
|
||
const {
|
||
value: counterValue,
|
||
loading: counterLoading,
|
||
error: counterError,
|
||
save: saveCounter,
|
||
refresh: refreshCounter
|
||
} = useKVValue<number>('demo:counter', true, true)
|
||
|
||
// Log initial state
|
||
React.useEffect(() => {
|
||
addLog('CloudflareKVDemo component initialized')
|
||
addLog(`KV loading state: ${kv.isLoading}`)
|
||
addLog(`KV has error: ${kv.hasError}`)
|
||
if (kv.hasError) {
|
||
addLog(`KV errors: ${JSON.stringify(kv.errors)}`)
|
||
}
|
||
}, [kv.isLoading, kv.hasError, kv.errors])
|
||
|
||
// Handle putting a value
|
||
const handlePutValue = async () => {
|
||
if (!inputKey || !inputValue) {
|
||
addLog('❌ Put operation failed: Key or value is empty')
|
||
alert('❌ Please enter both key and value')
|
||
return
|
||
}
|
||
|
||
addLog(`🔄 Starting put operation for key: ${inputKey}`)
|
||
|
||
try {
|
||
const data: DemoData = {
|
||
message: inputValue,
|
||
timestamp: new Date().toISOString(),
|
||
counter: (typeof counterValue === 'number' ? counterValue : 0) + 1
|
||
}
|
||
|
||
addLog(`📤 Sending data: ${JSON.stringify(data)}`)
|
||
const result = await kv.put(inputKey, data)
|
||
addLog(`✅ Put operation successful: ${JSON.stringify(result)}`)
|
||
alert('✅ Value stored successfully!')
|
||
} catch (error) {
|
||
const errorMessage = (error as Error).message
|
||
addLog(`❌ Put operation failed: ${errorMessage}`)
|
||
alert('❌ Failed to store value: ' + errorMessage)
|
||
}
|
||
}
|
||
|
||
// Handle getting a value
|
||
const handleGetValue = async () => {
|
||
if (!inputKey) {
|
||
addLog('❌ Get operation failed: Key is empty')
|
||
alert('❌ Please enter a key')
|
||
return
|
||
}
|
||
|
||
addLog(`🔄 Starting get operation for key: ${inputKey}`)
|
||
|
||
try {
|
||
const result = await kv.get<DemoData>(inputKey)
|
||
if (result) {
|
||
addLog(`✅ Get operation successful: ${JSON.stringify(result)}`)
|
||
alert(`✅ Retrieved value: ${JSON.stringify(result, null, 2)}`)
|
||
} else {
|
||
addLog(`ℹ️ Get operation completed: Key not found`)
|
||
alert('ℹ️ Key not found')
|
||
}
|
||
} catch (error) {
|
||
const errorMessage = (error as Error).message
|
||
addLog(`❌ Get operation failed: ${errorMessage}`)
|
||
alert('❌ Failed to get value: ' + errorMessage)
|
||
}
|
||
}
|
||
|
||
// Handle deleting a value
|
||
const handleDeleteValue = async () => {
|
||
if (!inputKey) {
|
||
addLog('❌ Delete operation failed: Key is empty')
|
||
alert('❌ Please enter a key')
|
||
return
|
||
}
|
||
|
||
addLog(`🔄 Starting delete operation for key: ${inputKey}`)
|
||
|
||
try {
|
||
const result = await kv.delete(inputKey)
|
||
addLog(`✅ Delete operation successful: ${JSON.stringify(result)}`)
|
||
alert('✅ Value deleted successfully!')
|
||
} catch (error) {
|
||
const errorMessage = (error as Error).message
|
||
addLog(`❌ Delete operation failed: ${errorMessage}`)
|
||
alert('❌ Failed to delete value: ' + errorMessage)
|
||
}
|
||
}
|
||
|
||
// Handle listing keys
|
||
const handleListKeys = async () => {
|
||
addLog(`🔄 Starting list keys operation with prefix: ${searchPrefix}`)
|
||
|
||
try {
|
||
const result = await kv.listKeys(searchPrefix, 50)
|
||
addLog(`✅ List keys operation successful: Found ${result.result.keys.length} keys`)
|
||
const keyNames = result.result.keys.map(k => k.name).join('\n')
|
||
alert(`✅ Found keys:\n${keyNames || 'No keys found'}`)
|
||
} catch (error) {
|
||
const errorMessage = (error as Error).message
|
||
addLog(`❌ List keys operation failed: ${errorMessage}`)
|
||
alert('❌ Failed to list keys: ' + errorMessage)
|
||
}
|
||
}
|
||
|
||
// Handle incrementing counter
|
||
const handleIncrementCounter = async () => {
|
||
try {
|
||
const newValue = (typeof counterValue === 'number' ? counterValue : 0) + 1
|
||
await saveCounter(newValue)
|
||
} catch (error) {
|
||
alert('❌ Failed to increment counter: ' + (error as Error).message)
|
||
}
|
||
}
|
||
|
||
// Handle batch operations
|
||
const handleBatchPut = async () => {
|
||
try {
|
||
const entries = [
|
||
{ key: 'batch:item1', value: { name: 'Item 1', price: 10.99 } },
|
||
{ key: 'batch:item2', value: { name: 'Item 2', price: 15.50 } },
|
||
{ key: 'batch:item3', value: { name: 'Item 3', price: 8.75 } }
|
||
]
|
||
|
||
await kv.putBatch(entries)
|
||
alert('✅ Batch operation completed!')
|
||
} catch (error) {
|
||
alert('❌ Batch operation failed: ' + (error as Error).message)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="max-w-4xl mx-auto p-6 space-y-8">
|
||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||
<h1 className="text-3xl font-bold text-gray-900 mb-6">
|
||
Cloudflare KV Demo
|
||
</h1>
|
||
|
||
{/* Loading indicator */}
|
||
{kv.isLoading && (
|
||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||
<div className="flex items-center">
|
||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
|
||
<span className="text-blue-800">Processing...</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Error display */}
|
||
{kv.hasError && (
|
||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||
<h3 className="text-red-800 font-medium mb-2">Errors:</h3>
|
||
<ul className="text-red-700 text-sm space-y-1">
|
||
{kv.errors.get && <li>Get: {kv.errors.get}</li>}
|
||
{kv.errors.put && <li>Put: {kv.errors.put}</li>}
|
||
{kv.errors.delete && <li>Delete: {kv.errors.delete}</li>}
|
||
{kv.errors.list && <li>List: {kv.errors.list}</li>}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
{/* Counter Section */}
|
||
<div className="bg-gray-50 rounded-lg p-4 mb-6">
|
||
<h2 className="text-xl font-semibold text-gray-900 mb-4">
|
||
Auto-loaded Counter
|
||
</h2>
|
||
<div className="flex items-center space-x-4">
|
||
<div className="text-lg">
|
||
Current value:
|
||
<span className="font-mono font-bold ml-2">
|
||
{counterLoading ? '...' : counterValue || 0}
|
||
</span>
|
||
</div>
|
||
<button
|
||
onClick={handleIncrementCounter}
|
||
disabled={counterLoading}
|
||
className="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
|
||
>
|
||
Increment
|
||
</button>
|
||
<button
|
||
onClick={refreshCounter}
|
||
disabled={counterLoading}
|
||
className="bg-gray-500 hover:bg-gray-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
|
||
>
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
{counterError && (
|
||
<p className="text-red-600 text-sm mt-2">{counterError}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Manual Operations Section */}
|
||
<div className="space-y-6">
|
||
<h2 className="text-xl font-semibold text-gray-900">
|
||
Manual Operations
|
||
</h2>
|
||
|
||
{/* Key-Value Input */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Key
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={inputKey}
|
||
onChange={(e) => setInputKey(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="Enter key name"
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Value
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={inputValue}
|
||
onChange={(e) => setInputValue(e.target.value)}
|
||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="Enter value"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Action Buttons */}
|
||
<div className="flex flex-wrap gap-3">
|
||
<button
|
||
onClick={handlePutValue}
|
||
disabled={kv.isLoading || !inputKey || !inputValue}
|
||
className="bg-green-500 hover:bg-green-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
|
||
>
|
||
Put Value
|
||
</button>
|
||
<button
|
||
onClick={handleGetValue}
|
||
disabled={kv.isLoading || !inputKey}
|
||
className="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
|
||
>
|
||
Get Value
|
||
</button>
|
||
<button
|
||
onClick={handleDeleteValue}
|
||
disabled={kv.isLoading || !inputKey}
|
||
className="bg-red-500 hover:bg-red-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
|
||
>
|
||
Delete Value
|
||
</button>
|
||
</div>
|
||
|
||
{/* List Keys Section */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
Search Prefix
|
||
</label>
|
||
<div className="flex gap-3">
|
||
<input
|
||
type="text"
|
||
value={searchPrefix}
|
||
onChange={(e) => setSearchPrefix(e.target.value)}
|
||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||
placeholder="Enter prefix to search"
|
||
/>
|
||
<button
|
||
onClick={handleListKeys}
|
||
disabled={kv.isLoading}
|
||
className="bg-purple-500 hover:bg-purple-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
|
||
>
|
||
List Keys
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Batch Operations */}
|
||
<div>
|
||
<button
|
||
onClick={handleBatchPut}
|
||
disabled={kv.isLoading}
|
||
className="bg-orange-500 hover:bg-orange-600 disabled:bg-gray-400 text-white px-4 py-2 rounded-lg transition-colors"
|
||
>
|
||
Batch Put Demo Items
|
||
</button>
|
||
</div>
|
||
|
||
{/* Clear States */}
|
||
<div>
|
||
<button
|
||
onClick={kv.clearStates}
|
||
className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors"
|
||
>
|
||
Clear All States
|
||
</button>
|
||
<button
|
||
onClick={() => setLogs([])}
|
||
className="bg-gray-500 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors ml-3"
|
||
>
|
||
Clear Logs
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Debug Logs Section */}
|
||
<div className="bg-white rounded-lg shadow-lg p-6 mt-6">
|
||
<h2 className="text-xl font-semibold text-gray-900 mb-4">
|
||
Debug Logs
|
||
</h2>
|
||
<div className="bg-gray-900 text-green-400 p-4 rounded-lg font-mono text-sm max-h-96 overflow-y-auto">
|
||
{logs.length === 0 ? (
|
||
<div className="text-gray-500">No logs yet...</div>
|
||
) : (
|
||
logs.map((log, index) => (
|
||
<div key={index} className="mb-1">
|
||
{log}
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default CloudflareKVDemo
|