-
Notifications
You must be signed in to change notification settings - Fork 30
feat(rating): add RatingStars, RatingModal; integrate maintainer rati… #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
09e49c6
0eb6416
fa54563
93af1fd
d216b3f
a0bfcdd
e6ab1d5
38d0d8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "use client" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useMemo, useState } from "react" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { RatingModal } from "../rating/rating-modal" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from "@/components/ui/button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Separator } from "@/components/ui/separator" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Bounty } from "@/types/bounty" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -20,8 +21,9 @@ export function BountySidebar({ bounty }: BountySidebarProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // const router = useRouter() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mock user ID for now - in real app this comes from auth context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Mock user ID and maintainer check for now - in real app this comes from auth context | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CURRENT_USER_ID = "mock-user-123" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const IS_MAINTAINER = true // TODO: Replace with real maintainer check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // const isClaimable = bounty.status === "open" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -75,7 +77,63 @@ export function BountySidebar({ bounty }: BountySidebarProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Rating modal state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [showRating, setShowRating] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [completed, setCompleted] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [lastRating, setLastRating] = useState<number | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [reputationGain, setReputationGain] = useState<number | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [hasRated, setHasRated] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleMarkCompleted = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!IS_MAINTAINER) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert('Only maintainers can mark as completed.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Simulate completion API call | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setCompleted(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setShowRating(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 1000) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleSubmitRating = async (rating: number, feedback: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (hasRated) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert('You have already rated this contributor.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!IS_MAINTAINER) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert('Only maintainers can rate contributors.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!completed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert('Bounty must be marked as completed before rating.'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -name "bounty-sidebar.tsx" -type fRepository: boundlessfi/bounties Length of output: 102 🏁 Script executed: find . -name "*rating*modal*" -o -name "*RatingModal*" | grep -E "\.(tsx?|jsx?)$"Repository: boundlessfi/bounties Length of output: 100 🏁 Script executed: find . -type f -name "*.tsx" | xargs grep -l "RatingModal" | head -20Repository: boundlessfi/bounties Length of output: 139 🏁 Script executed: wc -l ./components/bounty/bounty-sidebar.tsxRepository: boundlessfi/bounties Length of output: 106 🏁 Script executed: cat -n ./components/bounty/bounty-sidebar.tsx | grep -A 50 "handleSubmitRating"Repository: boundlessfi/bounties Length of output: 4539 🏁 Script executed: wc -l ./components/rating/rating-modal.tsxRepository: boundlessfi/bounties Length of output: 103 🏁 Script executed: cat -n ./components/rating/rating-modal.tsxRepository: boundlessfi/bounties Length of output: 2888 Guard failures should reject so the modal doesn't show success.
🛠️ Suggested fix if (hasRated) {
alert('You have already rated this contributor.');
- return;
+ throw new Error('You have already rated this contributor.');
}
if (!IS_MAINTAINER) {
alert('Only maintainers can rate contributors.');
- return;
+ throw new Error('Only maintainers can rate contributors.');
}
if (!completed) {
alert('Bounty must be marked as completed before rating.');
- return;
+ throw new Error('Bounty must be marked as completed before rating.');
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Simulate API call to reputation endpoint and calculate points | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await new Promise((res) => setTimeout(res, 1000)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Use feedback variable to avoid unused variable lint warnings | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| void feedback | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLastRating(rating) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setReputationGain(rating * 10) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setHasRated(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setShowRating(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Notify contributor (mock) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.success(`You have been rated ${rating} star${rating > 1 ? 's' : ''} and gained +${rating * 10} reputation!`, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: 'Congratulations on your contribution!' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const renderActionButton = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (bounty.status === 'claimed' && IS_MAINTAINER && !completed) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button onClick={handleMarkCompleted} disabled={loading} className="w-full gap-2 bg-green-600 text-white hover:bg-green-700"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Mark as Completed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
141
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Allow reopening the rating flow after completion. Once the modal is dismissed, there’s no way to rate later because the “Mark as Completed” button disappears and no “Rate Contributor” action is shown. This blocks a core flow if the maintainer closes the modal accidentally. 🛠️ Suggested fix (add a “Rate Contributor” action) const renderActionButton = () => {
if (bounty.status === 'claimed' && IS_MAINTAINER && !completed) {
return (
<Button onClick={handleMarkCompleted} disabled={loading} className="w-full gap-2 bg-green-600 text-white hover:bg-green-700">
{loading ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Check className="mr-2 h-4 w-4" />}
Mark as Completed
</Button>
)
}
+ if (bounty.status === 'claimed' && IS_MAINTAINER && completed && !hasRated) {
+ return (
+ <Button onClick={() => setShowRating(true)} className="w-full gap-2 bg-primary text-primary-foreground hover:bg-primary/90">
+ Rate Contributor
+ </Button>
+ )
+ }🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (bounty.status !== 'open') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const labels: Record<string, string> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| claimed: 'Already Claimed', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -155,6 +213,23 @@ export function BountySidebar({ bounty }: BountySidebarProps) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="sticky top-4 rounded-xl border border-gray-800 bg-background-card p-6 space-y-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Sidebar UI */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {showRating && !hasRated && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <RatingModal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contributor={{ id: bounty.claimedBy || '', name: 'Contributor', reputation: 100 + (reputationGain || 0) }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bounty={{ id: bounty.id, title: bounty.issueTitle }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSubmit={handleSubmitRating} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClose={() => setShowRating(false)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* Show rating and reputation gain after rating, visible to all users if available */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {lastRating && reputationGain && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-4 mb-4 rounded bg-green-900/60 text-green-200 border border-green-700"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mb-1">{IS_MAINTAINER ? 'You rated the contributor:' : 'Contributor was rated:'} <b>{lastRating} / 5</b> stars</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div>Reputation gained: <b>+{reputationGain}</b></div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button asChild className="w-full gap-2 bg-primary text-primary-foreground hover:bg-primary/90"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <a href={bounty.githubIssueUrl} target="_blank" rel="noopener noreferrer"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Github className="size-4" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import React, { useState } from 'react'; | ||
| import { RatingStars } from './rating-stars'; | ||
|
|
||
| interface RatingModalProps { | ||
| contributor: { | ||
| id: string; | ||
| name: string; | ||
| reputation: number; | ||
| }; | ||
| bounty: { | ||
| id: string; | ||
| title: string; | ||
| }; | ||
| onSubmit: (rating: number, feedback: string) => Promise<void>; | ||
| onClose: () => void; | ||
| } | ||
|
|
||
| export const RatingModal: React.FC<RatingModalProps> = ({ contributor, bounty, onSubmit, onClose }) => { | ||
| const [rating, setRating] = useState(0); | ||
| const [feedback, setFeedback] = useState(''); | ||
| const [loading, setLoading] = useState(false); | ||
| const [error, setError] = useState<string | null>(null); | ||
| const [success, setSuccess] = useState(false); | ||
|
|
||
| const handleSubmit = async () => { | ||
| if (rating < 1 || rating > 5) { | ||
| setError('Please select a rating between 1 and 5.'); | ||
| return; | ||
| } | ||
| setLoading(true); | ||
| setError(null); | ||
| try { | ||
| await onSubmit(rating, feedback); | ||
| setSuccess(true); | ||
| } catch (err) { | ||
| console.error(err) | ||
| setError('Failed to submit rating. Please try again.'); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }; | ||
|
|
||
| if (success) { | ||
| return ( | ||
| <div className="modal"> | ||
| <h2>Success!</h2> | ||
| <p>Rating submitted. Contributor reputation updated.</p> | ||
| <button onClick={onClose}>Close</button> | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <div className="modal"> | ||
| <h2>Rate Contributor</h2> | ||
| <div> | ||
| <strong>Bounty:</strong> {bounty.title} | ||
| </div> | ||
| <div> | ||
| <strong>Contributor:</strong> {contributor.name} | ||
| </div> | ||
| <div> | ||
| <strong>Current Reputation:</strong> {contributor.reputation} | ||
| </div> | ||
| <div style={{ margin: '16px 0' }}> | ||
| <RatingStars value={rating} onChange={setRating} /> | ||
| </div> | ||
| <textarea | ||
| placeholder="Optional feedback" | ||
| value={feedback} | ||
| onChange={e => setFeedback(e.target.value)} | ||
| rows={3} | ||
| style={{ width: '100%', marginBottom: 8 }} | ||
| /> | ||
| {error && <div style={{ color: 'red', marginBottom: 8 }}>{error}</div>} | ||
| <button onClick={handleSubmit} disabled={loading}> | ||
| {loading ? 'Submitting...' : 'Submit'} | ||
| </button> | ||
| <button onClick={onClose} style={{ marginLeft: 8 }}>Cancel</button> | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import React, { useState } from 'react'; | ||
|
|
||
| interface RatingStarsProps { | ||
| value: number; | ||
| onChange?: (value: number) => void; | ||
| disabled?: boolean; | ||
| displayOnly?: boolean; | ||
| } | ||
|
|
||
| export const RatingStars: React.FC<RatingStarsProps> = ({ value, onChange, disabled, displayOnly }) => { | ||
| const [hovered, setHovered] = useState<number | null>(null); | ||
|
|
||
| const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => { | ||
| if (!onChange || disabled || displayOnly) return; | ||
| if (e.key === 'ArrowLeft' && value > 1) onChange(value - 1); | ||
| if (e.key === 'ArrowRight' && value < 5) onChange(value + 1); | ||
| }; | ||
|
|
||
| return ( | ||
| <div | ||
| tabIndex={displayOnly ? -1 : 0} | ||
| role={displayOnly ? 'img' : 'slider'} | ||
| aria-valuenow={value} | ||
| aria-valuemin={1} | ||
| aria-valuemax={5} | ||
| onKeyDown={handleKeyDown} | ||
| style={{ display: 'flex', gap: 4, outline: 'none', cursor: displayOnly ? 'default' : 'pointer' }} | ||
| > | ||
| {[1, 2, 3, 4, 5].map((star) => ( | ||
| <span | ||
| key={star} | ||
| onMouseEnter={() => !displayOnly && setHovered(star)} | ||
| onMouseLeave={() => !displayOnly && setHovered(null)} | ||
| onClick={() => onChange && !disabled && !displayOnly && onChange(star)} | ||
| style={{ | ||
| color: (hovered ?? value) >= star ? '#FFD700' : '#CCC', | ||
| fontSize: 28, | ||
| transition: 'color 0.2s', | ||
| pointerEvents: displayOnly ? 'none' : 'auto', | ||
| userSelect: 'none', | ||
| }} | ||
|
Comment on lines
+10
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: find . -type f -name "rating-stars.tsx" | head -20Repository: boundlessfi/bounties Length of output: 100 🏁 Script executed: cat -n ./components/rating/rating-stars.tsxRepository: boundlessfi/bounties Length of output: 1975 Ensure When Suggested fix export const RatingStars: React.FC<RatingStarsProps> = ({ value, onChange, disabled, displayOnly }) => {
const [hovered, setHovered] = useState<number | null>(null);
+ const isDisplayOnly = displayOnly || !onChange;
+ const isInteractive = !isDisplayOnly && !disabled;
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
- if (!onChange || disabled || displayOnly) return;
+ if (!isInteractive) return;
if (e.key === 'ArrowLeft' && value > 1) onChange(value - 1);
if (e.key === 'ArrowRight' && value < 5) onChange(value + 1);
};
return (
<div
- tabIndex={displayOnly ? -1 : 0}
- role={displayOnly ? 'img' : 'slider'}
- aria-valuenow={value}
- aria-valuemin={1}
- aria-valuemax={5}
+ tabIndex={isInteractive ? 0 : -1}
+ role={isDisplayOnly ? 'img' : 'slider'}
+ aria-label={isDisplayOnly ? `${value} out of 5 stars` : 'Rating'}
+ aria-valuenow={isDisplayOnly ? undefined : value}
+ aria-valuemin={isDisplayOnly ? undefined : 0}
+ aria-valuemax={isDisplayOnly ? undefined : 5}
+ aria-disabled={isDisplayOnly ? undefined : disabled}
onKeyDown={handleKeyDown}
- style={{ display: 'flex', gap: 4, outline: 'none', cursor: displayOnly ? 'default' : 'pointer' }}
+ style={{ display: 'flex', gap: 4, outline: 'none', cursor: isInteractive ? 'pointer' : 'default' }}
>
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
- onMouseEnter={() => !displayOnly && setHovered(star)}
- onMouseLeave={() => !displayOnly && setHovered(null)}
- onClick={() => onChange && !disabled && !displayOnly && onChange(star)}
+ onMouseEnter={() => isInteractive && setHovered(star)}
+ onMouseLeave={() => isInteractive && setHovered(null)}
+ onClick={() => isInteractive && onChange?.(star)}
style={{
color: (hovered ?? value) >= star ? '#FFD700' : '#CCC',
fontSize: 28,
transition: 'color 0.2s',
- pointerEvents: displayOnly ? 'none' : 'auto',
+ pointerEvents: isInteractive ? 'auto' : 'none',
userSelect: 'none',
}}
aria-label={star + ' star'}🤖 Prompt for AI Agents |
||
| aria-label={star + ' star'} | ||
| > | ||
| ★ | ||
| </span> | ||
| ))} | ||
| </div> | ||
| ); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| // Ensure localStorage methods are configurable and writable so tests can spyOn/set mocks | ||
| const createLocalStorageMock = () => { | ||
| let store = {} | ||
| return { | ||
| getItem: (key) => (Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null), | ||
| setItem: (key, value) => { store[String(key)] = String(value) }, | ||
| removeItem: (key) => { delete store[String(key)] }, | ||
| clear: () => { store = {} }, | ||
| } | ||
| } | ||
|
|
||
| Object.defineProperty(window, 'localStorage', { | ||
| configurable: true, | ||
| writable: true, | ||
| value: createLocalStorageMock(), | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.