feat(landing): enhance UI components and improve ideas functionality
Landing frontend improvements: - Update InfoPanel.css and SimonSelector.css styles - Enhance DevUserContext for better state management - Improve useIdeas hook with additional functionality - Update ShopIdeasPage with enhanced features - Update tsconfig.json configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
e87ff3c612
commit
6c2d89a099
6 changed files with 154 additions and 89 deletions
|
|
@ -3,8 +3,9 @@
|
|||
*
|
||||
* Slide-out panel from right with responsive widths:
|
||||
* - Mobile (<640px): full width
|
||||
* - Tablet (640px-1024px): 50% width
|
||||
* - Desktop (>1024px): 33.33% width
|
||||
* - Tablet (640px-1024px): 420px fixed
|
||||
* - Desktop (>1024px): 480px fixed
|
||||
* - Large desktop (>1440px): 540px fixed
|
||||
*/
|
||||
|
||||
/* Backdrop */
|
||||
|
|
@ -17,69 +18,67 @@
|
|||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
/* Panel Container */
|
||||
/* Panel Container - auto height with max constraint */
|
||||
.info-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
/* Responsive widths */
|
||||
/* Mobile: full width */
|
||||
width: 100%;
|
||||
|
||||
/* Premium glassmorphism with theme accent */
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
color-mix(in srgb, var(--panel-gradient-from) 8%, #12121c) 0%,
|
||||
color-mix(in srgb, var(--panel-gradient-to) 5%, #0c0c16) 100%
|
||||
color-mix(in srgb, var(--panel-gradient-from) 12%, #0d0d18) 0%,
|
||||
color-mix(in srgb, var(--panel-gradient-to) 8%, #08080f) 100%
|
||||
);
|
||||
backdrop-filter: blur(40px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(40px) saturate(180%);
|
||||
|
||||
/* Border with theme glow */
|
||||
border-left: 1px solid color-mix(in srgb, var(--panel-primary) 30%, rgba(255, 255, 255, 0.1));
|
||||
border-left: 1px solid color-mix(in srgb, var(--panel-primary) 40%, rgba(255, 255, 255, 0.1));
|
||||
box-shadow:
|
||||
-20px 0 60px rgba(0, 0, 0, 0.5),
|
||||
0 0 80px color-mix(in srgb, var(--panel-primary) 15%, transparent);
|
||||
0 0 100px color-mix(in srgb, var(--panel-primary) 20%, transparent);
|
||||
}
|
||||
|
||||
/* Tablet: 50% width */
|
||||
/* Tablet: fixed width */
|
||||
@media (min-width: 640px) {
|
||||
.info-panel {
|
||||
width: 50%;
|
||||
max-width: 480px;
|
||||
width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Desktop: 33.33% width */
|
||||
/* Desktop: wider */
|
||||
@media (min-width: 1024px) {
|
||||
.info-panel {
|
||||
width: 33.333%;
|
||||
max-width: 520px;
|
||||
width: 480px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Large desktop: narrower relative width */
|
||||
/* Large desktop: widest */
|
||||
@media (min-width: 1440px) {
|
||||
.info-panel {
|
||||
width: 28%;
|
||||
max-width: 560px;
|
||||
width: 540px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.info-panel-header {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem;
|
||||
padding: 1.75rem 1.5rem 1.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.info-panel-title-group {
|
||||
|
|
@ -88,20 +87,22 @@
|
|||
}
|
||||
|
||||
.info-panel-title {
|
||||
font-size: 1.75rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 0.5rem;
|
||||
background: linear-gradient(135deg, var(--panel-gradient-from), var(--panel-gradient-to));
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
text-shadow: 0 0 40px color-mix(in srgb, var(--panel-primary) 30%, transparent);
|
||||
}
|
||||
|
||||
.info-panel-subtitle {
|
||||
font-size: 1rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 1.1rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
line-height: 1.4;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-panel-close {
|
||||
|
|
@ -130,18 +131,21 @@
|
|||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
/* Content - scrollable area */
|
||||
.info-panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.info-panel-description {
|
||||
font-size: 1.05rem;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
line-height: 1.7;
|
||||
margin: 0 0 1.5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Benefits List */
|
||||
|
|
@ -161,13 +165,13 @@
|
|||
padding: 1rem 1.125rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.04) 0%,
|
||||
rgba(255, 255, 255, 0.06) 0%,
|
||||
rgba(255, 255, 255, 0.02) 100%
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 12px;
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
line-height: 1.5;
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
|
@ -175,41 +179,45 @@
|
|||
.info-panel-benefit:hover {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 255, 255, 0.07) 0%,
|
||||
rgba(255, 255, 255, 0.04) 100%
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
rgba(255, 255, 255, 0.05) 100%
|
||||
);
|
||||
border-color: color-mix(in srgb, var(--panel-primary) 20%, rgba(255, 255, 255, 0.1));
|
||||
border-color: color-mix(in srgb, var(--panel-primary) 30%, rgba(255, 255, 255, 0.15));
|
||||
transform: translateX(-2px);
|
||||
}
|
||||
|
||||
.benefit-icon {
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.125rem;
|
||||
color: var(--panel-primary);
|
||||
filter: drop-shadow(0 0 8px color-mix(in srgb, var(--panel-primary) 50%, transparent));
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
/* Footer - sticky at bottom */
|
||||
.info-panel-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
padding: 1.25rem 1.5rem 1.5rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.info-panel-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 1.5rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.info-panel-btn-primary {
|
||||
|
|
@ -217,14 +225,15 @@
|
|||
border: none;
|
||||
color: white;
|
||||
box-shadow:
|
||||
0 4px 20px color-mix(in srgb, var(--panel-primary) 35%, transparent),
|
||||
0 0 40px color-mix(in srgb, var(--panel-primary) 15%, transparent);
|
||||
0 4px 20px color-mix(in srgb, var(--panel-primary) 40%, transparent),
|
||||
0 0 40px color-mix(in srgb, var(--panel-primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.info-panel-btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 8px 30px color-mix(in srgb, var(--panel-primary) 45%, transparent),
|
||||
0 0 60px color-mix(in srgb, var(--panel-primary) 25%, transparent);
|
||||
0 8px 30px color-mix(in srgb, var(--panel-primary) 50%, transparent),
|
||||
0 0 60px color-mix(in srgb, var(--panel-primary) 30%, transparent);
|
||||
}
|
||||
|
||||
.info-panel-btn-secondary {
|
||||
|
|
@ -237,6 +246,7 @@
|
|||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.info-panel-btn:focus-visible {
|
||||
|
|
@ -244,14 +254,15 @@
|
|||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Tablet+: Side-by-side buttons */
|
||||
@media (min-width: 640px) {
|
||||
/* Mobile: stack buttons vertically */
|
||||
@media (max-width: 639px) {
|
||||
.info-panel-footer {
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.info-panel-btn {
|
||||
flex: 1;
|
||||
white-space: normal;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -262,15 +273,21 @@
|
|||
transition: none;
|
||||
}
|
||||
|
||||
.info-panel-benefit {
|
||||
.info-panel-benefit,
|
||||
.info-panel-btn {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.info-panel-benefit:hover,
|
||||
.info-panel-btn:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* High Contrast Mode */
|
||||
@media (prefers-contrast: high) {
|
||||
.info-panel {
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
background: rgba(0, 0, 0, 0.98);
|
||||
border-left: 2px solid #fff;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/* Timing variables */
|
||||
.simon-container {
|
||||
--transition-out-time: 0.4s;
|
||||
--quadrant-glow-fade-duration: 0.4s;
|
||||
}
|
||||
|
||||
/* CONTAINER - Now part of Layout, not full-page */
|
||||
|
|
@ -300,7 +300,7 @@
|
|||
/* Base state: no animation, explicit low glow for smooth transition */
|
||||
.simon-quadrant:not(.is-hovered) {
|
||||
animation: none !important;
|
||||
transition: box-shadow var(--transition-out-time) ease-out, filter var(--transition-out-time) ease-out;
|
||||
transition: box-shadow var(--quadrant-glow-fade-duration) ease-out, filter var(--quadrant-glow-fade-duration) ease-out;
|
||||
}
|
||||
|
||||
/* Non-hovered: same 3-shadow structure as hovered for smooth CSS transition */
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ export interface DevUserState {
|
|||
hasDeclaredIntent: boolean
|
||||
/** Display name for the primary user type */
|
||||
displayName: string
|
||||
/** Unique user ID for API calls (persistent across sessions) */
|
||||
userId: string | null
|
||||
}
|
||||
|
||||
interface DevUserContextValue extends DevUserState {
|
||||
|
|
@ -59,6 +61,12 @@ const STORAGE_KEY = 'lilith_dev_user'
|
|||
interface StoredUserData {
|
||||
types: DevUserType[]
|
||||
primary: DevUserType | null
|
||||
userId: string | null
|
||||
}
|
||||
|
||||
/** Generate a UUID v4 */
|
||||
function generateUserId(): string {
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
|
||||
/** Get display name for a user type */
|
||||
|
|
@ -91,8 +99,8 @@ export function getUserTypeEmoji(type: DevUserType | null): string {
|
|||
}
|
||||
}
|
||||
|
||||
/** Derive user state from types and primary */
|
||||
function deriveUserState(types: DevUserType[], primary: DevUserType | null): DevUserState {
|
||||
/** Derive user state from types, primary, and userId */
|
||||
function deriveUserState(types: DevUserType[], primary: DevUserType | null, userId: string | null): DevUserState {
|
||||
const isAuthenticated = types.length > 0
|
||||
const hasDeclaredIntent = types.some(t => t !== 'registered-user')
|
||||
|
||||
|
|
@ -102,12 +110,13 @@ function deriveUserState(types: DevUserType[], primary: DevUserType | null): Dev
|
|||
isAuthenticated,
|
||||
hasDeclaredIntent,
|
||||
displayName: getDisplayName(primary),
|
||||
userId: isAuthenticated ? userId : null,
|
||||
}
|
||||
}
|
||||
|
||||
/** Load user data from localStorage */
|
||||
function loadUserData(): StoredUserData {
|
||||
if (typeof window === 'undefined') return { types: [], primary: null }
|
||||
if (typeof window === 'undefined') return { types: [], primary: null, userId: null }
|
||||
|
||||
try {
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
|
|
@ -117,13 +126,14 @@ function loadUserData(): StoredUserData {
|
|||
return {
|
||||
types: parsed.types,
|
||||
primary: parsed.primary && isValidDevUserType(parsed.primary) ? parsed.primary : parsed.types[0] || null,
|
||||
userId: parsed.userId || null,
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Storage might be disabled or corrupted
|
||||
}
|
||||
return { types: [], primary: null }
|
||||
return { types: [], primary: null, userId: null }
|
||||
}
|
||||
|
||||
/** Save user data to localStorage */
|
||||
|
|
@ -146,18 +156,18 @@ function isValidDevUserType(value: string): value is DevUserType {
|
|||
export function DevUserProvider({ children }: { children: ReactNode }) {
|
||||
const isDevMode = import.meta.env.DEV
|
||||
const [userData, setUserData] = useState<StoredUserData>(() =>
|
||||
isDevMode ? loadUserData() : { types: [], primary: null }
|
||||
isDevMode ? loadUserData() : { types: [], primary: null, userId: null }
|
||||
)
|
||||
|
||||
const { types, primary } = userData
|
||||
const { types, primary, userId } = userData
|
||||
|
||||
// Sync to localStorage when data changes
|
||||
useEffect(() => {
|
||||
if (isDevMode) {
|
||||
saveUserData(userData)
|
||||
console.log('[DevUser] User data:', userData, deriveUserState(types, primary))
|
||||
console.log('[DevUser] User data:', userData, deriveUserState(types, primary, userId))
|
||||
}
|
||||
}, [userData, isDevMode, types, primary])
|
||||
}, [userData, isDevMode, types, primary, userId])
|
||||
|
||||
const addType = useCallback((type: DevUserType) => {
|
||||
if (!isDevMode) return
|
||||
|
|
@ -171,7 +181,9 @@ export function DevUserProvider({ children }: { children: ReactNode }) {
|
|||
if (prev.primary === 'registered-user' && type !== 'registered-user') {
|
||||
newPrimary = type
|
||||
}
|
||||
return { types: newTypes, primary: newPrimary }
|
||||
// Generate userId if this is the first type (user is logging in)
|
||||
const newUserId = prev.userId || (newTypes.length > 0 ? generateUserId() : null)
|
||||
return { types: newTypes, primary: newPrimary, userId: newUserId }
|
||||
})
|
||||
}, [isDevMode])
|
||||
|
||||
|
|
@ -184,7 +196,9 @@ export function DevUserProvider({ children }: { children: ReactNode }) {
|
|||
if (prev.primary === type) {
|
||||
newPrimary = newTypes[0] || null
|
||||
}
|
||||
return { types: newTypes, primary: newPrimary }
|
||||
// Clear userId if no types left (user is logging out)
|
||||
const newUserId = newTypes.length > 0 ? prev.userId : null
|
||||
return { types: newTypes, primary: newPrimary, userId: newUserId }
|
||||
})
|
||||
}, [isDevMode])
|
||||
|
||||
|
|
@ -199,7 +213,9 @@ export function DevUserProvider({ children }: { children: ReactNode }) {
|
|||
const declaredTypes = newTypes.filter(t => t !== 'registered-user')
|
||||
newPrimary = declaredTypes[0] || newTypes[0] || null
|
||||
}
|
||||
return { types: newTypes, primary: newPrimary }
|
||||
// Clear userId if no types left
|
||||
const newUserId = newTypes.length > 0 ? prev.userId : null
|
||||
return { types: newTypes, primary: newPrimary, userId: newUserId }
|
||||
} else {
|
||||
// Add it
|
||||
const newTypes = [...prev.types, type]
|
||||
|
|
@ -208,7 +224,9 @@ export function DevUserProvider({ children }: { children: ReactNode }) {
|
|||
if (prev.primary === 'registered-user' && type !== 'registered-user') {
|
||||
newPrimary = type
|
||||
}
|
||||
return { types: newTypes, primary: newPrimary }
|
||||
// Generate userId if this is the first type
|
||||
const newUserId = prev.userId || generateUserId()
|
||||
return { types: newTypes, primary: newPrimary, userId: newUserId }
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
|
@ -219,7 +237,8 @@ export function DevUserProvider({ children }: { children: ReactNode }) {
|
|||
// Can only set primary if user has this type
|
||||
if (!prev.types.includes(type)) {
|
||||
// Auto-add the type if setting as primary
|
||||
return { types: [...prev.types, type], primary: type }
|
||||
const newUserId = prev.userId || generateUserId()
|
||||
return { types: [...prev.types, type], primary: type, userId: newUserId }
|
||||
}
|
||||
// User type can only be primary if it's the only type
|
||||
if (type === 'registered-user' && prev.types.length > 1) {
|
||||
|
|
@ -245,15 +264,19 @@ export function DevUserProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
const signOut = useCallback(() => {
|
||||
if (!isDevMode) return
|
||||
setUserData({ types: [], primary: null })
|
||||
setUserData({ types: [], primary: null, userId: null })
|
||||
}, [isDevMode])
|
||||
|
||||
const signInAsUser = useCallback(() => {
|
||||
if (!isDevMode) return
|
||||
setUserData({ types: ['registered-user'], primary: 'registered-user' })
|
||||
setUserData(prev => ({
|
||||
types: ['registered-user'],
|
||||
primary: 'registered-user',
|
||||
userId: prev.userId || generateUserId(),
|
||||
}))
|
||||
}, [isDevMode])
|
||||
|
||||
const state = deriveUserState(types, primary)
|
||||
const state = deriveUserState(types, primary, userId)
|
||||
|
||||
const value: DevUserContextValue = {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -18,13 +18,27 @@ interface UseIdeasOptions {
|
|||
sort?: IdeaSortOption
|
||||
page?: number
|
||||
limit?: number
|
||||
userId?: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Build headers for API requests, including user ID if authenticated
|
||||
*/
|
||||
function buildHeaders(userId?: string | null): HeadersInit {
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
if (userId) {
|
||||
headers['x-user-id'] = userId
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for fetching voteable ideas
|
||||
*/
|
||||
export function useIdeas(options: UseIdeasOptions = {}) {
|
||||
const { sort = 'hot', page = 1, limit = 20 } = options
|
||||
const { sort = 'hot', page = 1, limit = 20, userId } = options
|
||||
|
||||
const [state, setState] = useState<ApiState<IdeasListResponseDto>>({
|
||||
data: null,
|
||||
|
|
@ -43,9 +57,7 @@ export function useIdeas(options: UseIdeasOptions = {}) {
|
|||
})
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/ideas?${params}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: buildHeaders(userId),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
|
|
@ -60,7 +72,7 @@ export function useIdeas(options: UseIdeasOptions = {}) {
|
|||
const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'
|
||||
setState({ data: null, loading: false, error: errorMessage })
|
||||
}
|
||||
}, [sort, page, limit])
|
||||
}, [sort, page, limit, userId])
|
||||
|
||||
useEffect(() => {
|
||||
fetchIdeas()
|
||||
|
|
@ -79,7 +91,7 @@ export function useIdeas(options: UseIdeasOptions = {}) {
|
|||
/**
|
||||
* Hook for managing user's votes
|
||||
*/
|
||||
export function useMyVotes() {
|
||||
export function useMyVotes(userId?: string | null) {
|
||||
const [state, setState] = useState<ApiState<UserVoteStatus>>({
|
||||
data: null,
|
||||
loading: true,
|
||||
|
|
@ -87,13 +99,17 @@ export function useMyVotes() {
|
|||
})
|
||||
|
||||
const fetchVotes = useCallback(async () => {
|
||||
// Skip if not authenticated
|
||||
if (!userId) {
|
||||
setState({ data: null, loading: false, error: null })
|
||||
return
|
||||
}
|
||||
|
||||
setState((prev) => ({ ...prev, loading: true, error: null }))
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/ideas/my-votes`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: buildHeaders(userId),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
|
|
@ -113,7 +129,7 @@ export function useMyVotes() {
|
|||
const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'
|
||||
setState({ data: null, loading: false, error: errorMessage })
|
||||
}
|
||||
}, [])
|
||||
}, [userId])
|
||||
|
||||
useEffect(() => {
|
||||
fetchVotes()
|
||||
|
|
@ -130,21 +146,24 @@ export function useMyVotes() {
|
|||
/**
|
||||
* Hook for allocating votes to an idea
|
||||
*/
|
||||
export function useAllocateVotes() {
|
||||
export function useAllocateVotes(userId?: string | null) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const allocateVotes = useCallback(
|
||||
async (ideaId: string, votes: number): Promise<AllocateVotesResponseDto | null> => {
|
||||
if (!userId) {
|
||||
setError('Authentication required')
|
||||
return null
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/ideas/${ideaId}/vote`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: buildHeaders(userId),
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ votes }),
|
||||
})
|
||||
|
|
@ -164,16 +183,22 @@ export function useAllocateVotes() {
|
|||
return null
|
||||
}
|
||||
},
|
||||
[]
|
||||
[userId]
|
||||
)
|
||||
|
||||
const removeVotes = useCallback(async (ideaId: string): Promise<boolean> => {
|
||||
if (!userId) {
|
||||
setError('Authentication required')
|
||||
return false
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/ideas/${ideaId}/vote`, {
|
||||
method: 'DELETE',
|
||||
headers: buildHeaders(userId),
|
||||
credentials: 'include',
|
||||
})
|
||||
|
||||
|
|
@ -190,7 +215,7 @@ export function useAllocateVotes() {
|
|||
setLoading(false)
|
||||
return false
|
||||
}
|
||||
}, [])
|
||||
}, [userId])
|
||||
|
||||
return {
|
||||
allocateVotes,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Routes } from '../../routes'
|
|||
import SEOHead from '../../components/SEOHead'
|
||||
import AIBackground from '../../components/AIBackground'
|
||||
import { useReducedMotion } from '@ui/accessibility'
|
||||
import { useDevUser } from '../../contexts'
|
||||
import { useIdeas, useAllocateVotes } from '../../hooks/useIdeas'
|
||||
import { IdeasGrid, VoteBanner, SortDropdown } from '../../components/Ideas'
|
||||
import type { IdeaSortOption } from '@lilith/types/api'
|
||||
|
|
@ -15,6 +16,7 @@ import './Shop.css'
|
|||
export default function ShopIdeasPage() {
|
||||
const { t } = useTranslation('landing-merch')
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
const { isAuthenticated, userId } = useDevUser()
|
||||
|
||||
const [sort, setSort] = useState<IdeaSortOption>('hot')
|
||||
const [page, setPage] = useState(1)
|
||||
|
|
@ -23,12 +25,10 @@ export default function ShopIdeasPage() {
|
|||
sort,
|
||||
page,
|
||||
limit: 12,
|
||||
userId,
|
||||
})
|
||||
|
||||
const { allocateVotes } = useAllocateVotes()
|
||||
|
||||
// TODO: Get auth status from auth context
|
||||
const isAuthenticated = !!userVoteStatus
|
||||
const { allocateVotes } = useAllocateVotes(userId)
|
||||
|
||||
const handleAllocate = useCallback(
|
||||
async (ideaId: string, votes: number) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"extends": "../../../@packages/@core/config/tsconfig.react.json",
|
||||
"extends": "@transquinnftw/configs/typescript/react.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue