34 Commits

Author SHA1 Message Date
viper151
7d0fd141ff Merge pull request #203 from SyedaAnshrahGillani/fix-accessibility-issues
feat: Improve accessibility and refactor settings management
2025-10-01 22:21:35 +02:00
SyedaAnshrahGillani
e5a05d9865 fix: Address coderabbitai feedback 2025-10-02 01:12:52 +05:00
SyedaAnshrahGillani
2d6c3b5755 refactor: Create useLocalStorage hook to reduce code duplication 2025-10-01 17:16:09 +05:00
SyedaAnshrahGillani
2a5d27ffc0 fix(accessibility): Use buttons for modal backdrops 2025-10-01 17:04:35 +05:00
viper151
3c9a4cab82 Fix: Prevent CLI option injection in --print argument
Fix: Prevent CLI option injection in --print argument
2025-09-23 11:31:51 +02:00
viper151
ce9ab0cd16 Merge branch 'main' into fix-injection 2025-09-23 11:29:43 +02:00
viper151
66fad9a6a2 Update .env.example 2025-09-23 11:29:15 +02:00
viper151
0fcf906ff0 Merge pull request #197 from johnhenry/johnhenry/env-claude-path
Feat: Use environment variable for Claude path
2025-09-23 11:26:44 +02:00
viper151
7e1f2940d3 Merge branch 'main' into johnhenry/env-claude-path 2025-09-23 11:25:56 +02:00
simos
533d589132 Release 1.8.10 2025-09-23 10:54:35 +02:00
simos
d1f310161f fixes on changelog prettify 2025-09-23 10:54:00 +02:00
simos
b853e8cda1 Release 1.8.9 2025-09-23 10:51:45 +02:00
simos
1610de1f22 prettifying change logs 2025-09-23 10:51:18 +02:00
viper151
3f743d8210 Merge branch 'main' into johnhenry/env-claude-path 2025-09-23 10:48:59 +02:00
simos
cbb18fb010 Release 1.8.8 2025-09-23 10:38:53 +02:00
simos
d8d754274a modified: .release-it.json 2025-09-23 10:37:20 +02:00
simos
eb12aef641 modified: .release-it.json 2025-09-23 10:37:02 +02:00
simos
5e574dbdec modified: package.json 2025-09-23 10:36:04 +02:00
simos
af9e9eec02 modified: .release-it.json 2025-09-23 10:35:38 +02:00
simos
8c3ee770c3 fixing release-it 2025-09-23 10:35:14 +02:00
simos
58108c083c modified: package-lock.json 2025-09-23 10:33:08 +02:00
simos
af0ad6b4b6 fixes on npm 2025-09-23 10:28:24 +02:00
simos
36d9f47e29 Fixing release it 2025-09-23 10:24:55 +02:00
simos
1f25f1e79b Adding changelog to release-it 2025-09-23 10:22:16 +02:00
simos
d7ed1de1cb Release 1.8.6 2025-09-23 10:17:02 +02:00
simos
9be54233d0 fixs for npmjs package 2025-09-23 10:14:26 +02:00
simos
c8bcad71e7 Release 1.8.5 2025-09-23 02:16:12 +02:00
simos
a3f504aed2 adding executable 2025-09-23 02:15:32 +02:00
simos
f766ac1517 fixes 2025-09-23 02:13:36 +02:00
John Henry
d74a99ef24 Merge branch 'main' into johnhenry/env-claude-path 2025-09-22 10:53:48 -07:00
John Henry
133af82935 Merge branch 'main' into johnhenry/env-claude-path 2025-09-15 14:30:59 -07:00
viper151
3daf21c3d1 Merge branch 'main' into johnhenry/env-claude-path 2025-09-15 17:40:05 +02:00
John
f52ca8e702 Use environment variable for Claude path 2025-09-14 09:35:58 -07:00
Terrasse
d82a004224 fix prompt injection bug 2025-08-27 18:23:00 +08:00
9 changed files with 308 additions and 52 deletions

View File

@@ -9,4 +9,7 @@
#API server
PORT=3001
#Frontend port
VITE_PORT=5173
VITE_PORT=5173
# Uncomment the following line if you have a custom claude cli path other than the default "claude"
# CLAUDE_CLI_PATH=claude

21
.release-it.json Normal file
View File

@@ -0,0 +1,21 @@
{
"git": {
"commitMessage": "Release ${version}",
"tagName": "v${version}",
"changelog": "git log --pretty=format:\"* %s (%h)\" ${from}...${to}"
},
"npm": {
"publish": true
},
"github": {
"release": true,
"releaseName": "Claude Code UI v${version}",
"releaseNotes": {
"commit": "* ${commit.subject} (${sha}){ - thanks @${author.login}!}",
"excludeMatches": ["viper151"]
}
},
"hooks": {
"before:init": ["npm run build"]
}
}

View File

@@ -59,7 +59,17 @@ A desktop and mobile UI for [Claude Code](https://docs.anthropic.com/en/docs/cla
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) installed and configured, and/or
- [Cursor CLI](https://docs.cursor.com/en/cli/overview) installed and configured
### Installation
### One-click Operation (Recommended)
No installation required, direct operation:
```bash
npx @siteboon/claude-code-ui
```
Your default browser will automatically open the Claude Code UI interface.
### Local Development Installation
1. **Clone the repository:**
```bash

201
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "claude-code-ui",
"version": "1.8.4",
"name": "@siteboon/claude-code-ui",
"version": "1.8.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-code-ui",
"version": "1.8.4",
"name": "@siteboon/claude-code-ui",
"version": "1.8.10",
"license": "MIT",
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
@@ -16,6 +16,7 @@
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@siteboon/claude-code-ui": "^1.8.4",
"@tailwindcss/typography": "^0.5.16",
"@uiw/react-codemirror": "^4.23.13",
"@xterm/addon-clipboard": "^0.1.0",
@@ -46,10 +47,14 @@
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
},
"bin": {
"claude-code-ui": "server/index.js"
},
"devDependencies": {
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.6.0",
"auto-changelog": "^2.5.0",
"autoprefixer": "^10.4.16",
"concurrently": "^8.2.2",
"node-gyp": "^10.0.0",
@@ -2501,6 +2506,50 @@
"win32"
]
},
"node_modules/@siteboon/claude-code-ui": {
"version": "1.8.4",
"resolved": "https://registry.npmjs.org/@siteboon/claude-code-ui/-/claude-code-ui-1.8.4.tgz",
"integrity": "sha512-9moBlMDNF/6IfIcqShavxdq0TI9aNuY3+33YZcnvYagWsZMdJ/7d5tgDwAZEp3Uup/nHU+bdrkiXmFfLcRQLCQ==",
"license": "MIT",
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.9",
"@codemirror/lang-javascript": "^6.2.4",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-markdown": "^6.3.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/theme-one-dark": "^6.1.2",
"@tailwindcss/typography": "^0.5.16",
"@uiw/react-codemirror": "^4.23.13",
"@xterm/addon-clipboard": "^0.1.0",
"@xterm/addon-webgl": "^0.18.0",
"bcrypt": "^6.0.0",
"better-sqlite3": "^12.2.0",
"chokidar": "^4.0.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cors": "^2.8.5",
"cross-spawn": "^7.0.3",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.515.0",
"mime-types": "^3.0.1",
"multer": "^2.0.1",
"node-fetch": "^2.7.0",
"node-pty": "^1.1.0-beta34",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-markdown": "^10.1.0",
"react-router-dom": "^6.8.1",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7",
"tailwind-merge": "^3.3.1",
"ws": "^8.14.2",
"xterm": "^5.3.0",
"xterm-addon-fit": "^0.8.0"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
@@ -2982,6 +3031,50 @@
"node": ">=4"
}
},
"node_modules/auto-changelog": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/auto-changelog/-/auto-changelog-2.5.0.tgz",
"integrity": "sha512-UTnLjT7I9U2U/xkCUH5buDlp8C7g0SGChfib+iDrJkamcj5kaMqNKHNfbKJw1kthJUq8sUo3i3q2S6FzO/l/wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"commander": "^7.2.0",
"handlebars": "^4.7.7",
"import-cwd": "^3.0.0",
"node-fetch": "^2.6.1",
"parse-github-url": "^1.0.3",
"semver": "^7.3.5"
},
"bin": {
"auto-changelog": "src/index.js"
},
"engines": {
"node": ">=8.3"
}
},
"node_modules/auto-changelog/node_modules/commander": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/auto-changelog/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@@ -5122,6 +5215,28 @@
"devOptional": true,
"license": "ISC"
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@@ -5316,6 +5431,32 @@
],
"license": "BSD-3-Clause"
},
"node_modules/import-cwd": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-3.0.0.tgz",
"integrity": "sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"import-from": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/import-from": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/import-from/-/import-from-3.0.0.tgz",
"integrity": "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"resolve-from": "^5.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -7011,6 +7152,13 @@
"node": ">= 0.6"
}
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/netmask": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
@@ -7557,6 +7705,19 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/parse-github-url": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.3.tgz",
"integrity": "sha512-tfalY5/4SqGaV/GIGzWyHnFjlpTPTNpENR9Ea2lLldSJ8EWXMsvacWucqY3m3I4YPtas15IxTLQVQ5NSYXPrww==",
"dev": true,
"license": "MIT",
"bin": {
"parse-github-url": "cli.js"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/parse-path": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.1.0.tgz",
@@ -8368,6 +8529,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/restore-cursor": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
@@ -9181,7 +9352,6 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10257,6 +10427,20 @@
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
"license": "MIT"
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/undici": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.3.tgz",
@@ -10707,6 +10891,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",

View File

@@ -1,9 +1,12 @@
{
"name": "@siteboon/claude-code-ui",
"version": "1.8.4",
"version": "1.8.10",
"description": "A web-based UI for Claude Code CLI",
"type": "module",
"main": "server/index.js",
"bin": {
"claude-code-ui": "server/index.js"
},
"files": [
"server/",
"dist/",
@@ -77,6 +80,7 @@
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.6.0",
"auto-changelog": "^2.5.0",
"autoprefixer": "^10.4.16",
"concurrently": "^8.2.2",
"node-gyp": "^10.0.0",

View File

@@ -25,15 +25,6 @@ async function spawnClaude(command, options = {}, ws) {
// Build Claude CLI command - start with print/resume flags first
const args = [];
// Add print flag with command if we have a command
if (command && command.trim()) {
// Separate arguments for better cross-platform compatibility
// This prevents issues with spaces and quotes on Windows
args.push('--print');
args.push(command);
}
// Use cwd (actual project directory) instead of projectPath (Claude's metadata directory)
const workingDir = cwd || process.cwd();
@@ -225,6 +216,17 @@ async function spawnClaude(command, options = {}, ws) {
console.log('📝 Skip permissions disabled due to plan mode');
}
}
// Add print flag with command if we have a command
if (command && command.trim()) {
// Separate arguments for better cross-platform compatibility
// This prevents issues with spaces and quotes on Windows
args.push('--print');
// Use `--` so user input is always treated as text, not options
args.push('--');
args.push(command);
}
console.log('Spawning Claude CLI:', 'claude', args.map(arg => {
const cleanArg = arg.replace(/\n/g, '\\n').replace(/\r/g, '\\r');
@@ -235,7 +237,11 @@ async function spawnClaude(command, options = {}, ws) {
console.log('🔍 Full command args:', JSON.stringify(args, null, 2));
console.log('🔍 Final Claude command will be: claude ' + args.join(' '));
const claudeProcess = spawnFunction('claude', args, {
// Use Claude CLI from environment variable or default to 'claude'
const claudePath = process.env.CLAUDE_CLI_PATH || 'claude';
console.log('🔍 Using Claude CLI path:', claudePath);
const claudeProcess = spawnFunction(claudePath, args, {
cwd: workingDir,
stdio: ['pipe', 'pipe', 'pipe'],
env: { ...process.env } // Inherit all environment variables

View File

@@ -1,3 +1,4 @@
#!/usr/bin/env node
// Load environment variables from .env file
import fs from 'fs';
import path from 'path';

View File

@@ -33,6 +33,7 @@ import { TasksSettingsProvider } from './contexts/TasksSettingsContext';
import { WebSocketProvider, useWebSocketContext } from './contexts/WebSocketContext';
import ProtectedRoute from './components/ProtectedRoute';
import { useVersionCheck } from './hooks/useVersionCheck';
import useLocalStorage from './hooks/useLocalStorage';
import { api, authenticatedFetch } from './utils/api';
@@ -54,22 +55,10 @@ function AppContent() {
const [isInputFocused, setIsInputFocused] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showQuickSettings, setShowQuickSettings] = useState(false);
const [autoExpandTools, setAutoExpandTools] = useState(() => {
const saved = localStorage.getItem('autoExpandTools');
return saved !== null ? JSON.parse(saved) : false;
});
const [showRawParameters, setShowRawParameters] = useState(() => {
const saved = localStorage.getItem('showRawParameters');
return saved !== null ? JSON.parse(saved) : false;
});
const [autoScrollToBottom, setAutoScrollToBottom] = useState(() => {
const saved = localStorage.getItem('autoScrollToBottom');
return saved !== null ? JSON.parse(saved) : true;
});
const [sendByCtrlEnter, setSendByCtrlEnter] = useState(() => {
const saved = localStorage.getItem('sendByCtrlEnter');
return saved !== null ? JSON.parse(saved) : false;
});
const [autoExpandTools, setAutoExpandTools] = useLocalStorage('autoExpandTools', false);
const [showRawParameters, setShowRawParameters] = useLocalStorage('showRawParameters', false);
const [autoScrollToBottom, setAutoScrollToBottom] = useLocalStorage('autoScrollToBottom', true);
const [sendByCtrlEnter, setSendByCtrlEnter] = useLocalStorage('sendByCtrlEnter', false);
// Session Protection System: Track sessions with active conversations to prevent
// automatic project updates from interrupting ongoing chats. When a user sends
// a message, the session is marked as "active" and project updates are paused
@@ -491,9 +480,10 @@ function AppContent() {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
<button
className="fixed inset-0 bg-black/50 backdrop-blur-sm"
onClick={() => setShowVersionModal(false)}
aria-label="Close version upgrade modal"
/>
{/* Modal */}
@@ -604,7 +594,7 @@ function AppContent() {
<div className={`fixed inset-0 z-50 flex transition-all duration-150 ease-out ${
sidebarOpen ? 'opacity-100 visible' : 'opacity-0 invisible'
}`}>
<div
<button
className="fixed inset-0 bg-background/80 backdrop-blur-sm transition-opacity duration-150 ease-out"
onClick={(e) => {
e.stopPropagation();
@@ -615,6 +605,7 @@ function AppContent() {
e.stopPropagation();
setSidebarOpen(false);
}}
aria-label="Close sidebar"
/>
<div
className={`relative w-[85vw] max-w-sm sm:w-80 bg-card border-r border-border transform transition-transform duration-150 ease-out ${
@@ -688,25 +679,13 @@ function AppContent() {
isOpen={showQuickSettings}
onToggle={setShowQuickSettings}
autoExpandTools={autoExpandTools}
onAutoExpandChange={(value) => {
setAutoExpandTools(value);
localStorage.setItem('autoExpandTools', JSON.stringify(value));
}}
onAutoExpandChange={setAutoExpandTools}
showRawParameters={showRawParameters}
onShowRawParametersChange={(value) => {
setShowRawParameters(value);
localStorage.setItem('showRawParameters', JSON.stringify(value));
}}
onShowRawParametersChange={setShowRawParameters}
autoScrollToBottom={autoScrollToBottom}
onAutoScrollChange={(value) => {
setAutoScrollToBottom(value);
localStorage.setItem('autoScrollToBottom', JSON.stringify(value));
}}
onAutoScrollChange={setAutoScrollToBottom}
sendByCtrlEnter={sendByCtrlEnter}
onSendByCtrlEnterChange={(value) => {
setSendByCtrlEnter(value);
localStorage.setItem('sendByCtrlEnter', JSON.stringify(value));
}}
onSendByCtrlEnterChange={setSendByCtrlEnter}
isMobile={isMobile}
/>
)}

View File

@@ -0,0 +1,41 @@
import { useState } from 'react';
/**
* Custom hook to persist state in localStorage.
*
* @param {string} key The key to use for localStorage.
* @param {any} initialValue The initial value to use if nothing is in localStorage.
* @returns {[any, Function]} A tuple containing the stored value and a setter function.
*/
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.log(error);
return initialValue;
}
});
const setValue = (value) => {
if (typeof window === 'undefined') {
return;
}
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
setStoredValue(valueToStore);
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue];
}
export default useLocalStorage;