Skip to content

Commit 74bf95a

Browse files
committed
feat: add NewActionButton component for conditional button rendering based on user registration status
1 parent 2eac352 commit 74bf95a

5 files changed

Lines changed: 205 additions & 20 deletions

File tree

src/components/NewActionButton.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'
2+
3+
interface NewActionButtonProps {
4+
isRegistered: boolean
5+
onClick: () => void
6+
label: string
7+
icon?: string
8+
}
9+
10+
/**
11+
* Reusable button component that displays either an enabled action button or a disabled button with registration tooltip
12+
* @param isRegistered Whether the user is registered
13+
* @param onClick Function to call when the button is clicked (only if registered)
14+
* @param label Button text label
15+
* @param icon Optional icon class
16+
*/
17+
export function NewActionButton({
18+
isRegistered,
19+
onClick,
20+
label,
21+
icon = 'bi-plus-circle',
22+
}: NewActionButtonProps): React.JSX.Element {
23+
if (isRegistered) {
24+
return (
25+
<Button variant="primary" onClick={onClick} className="btn-sm d-flex align-items-center gap-2">
26+
<i className={`bi ${icon} d-flex align-items-center`}></i>
27+
{label}
28+
</Button>
29+
)
30+
}
31+
32+
return (
33+
<OverlayTrigger
34+
placement="left"
35+
overlay={
36+
<Tooltip id="register-tooltip">
37+
Please register your account on the Dashboard page before creating {label.toLowerCase().replace('new ', '')}
38+
</Tooltip>
39+
}
40+
>
41+
<span>
42+
<Button variant="primary" className="btn-sm d-flex align-items-center gap-2" disabled>
43+
<i className={`bi ${icon} d-flex align-items-center`}></i>
44+
{label}
45+
</Button>
46+
</span>
47+
</OverlayTrigger>
48+
)
49+
}

src/pages/MyApps.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useState, useEffect, useCallback } from 'react'
22
import { useAccount, useSignMessage } from 'wagmi'
3-
import { Alert, Button } from 'react-bootstrap'
4-
import { getMyApps, deleteApp, getAllTemplatesForUser } from '../services/api'
3+
import { Alert } from 'react-bootstrap'
4+
import { getMyApps, deleteApp, getAllTemplatesForUser, checkUserRegistration } from '../services/api'
55
import type { App, Template } from '../services/api'
66
import { AppList } from '../components/AppList'
77
import { Pagination } from '../components/Pagination'
88
import { CreateAppModal } from '../components/CreateAppModal'
99
import { TemplateSelectionModal } from '../components/TemplateSelectionModal'
10+
import { NewActionButton } from '../components/NewActionButton'
1011

1112
// Constants matching backend limitations
1213
const ITEMS_PER_PAGE = 12
@@ -25,6 +26,18 @@ export function MyApps(): React.JSX.Element {
2526
const [totalPages, setTotalPages] = useState(1)
2627
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null)
2728
const [error, setError] = useState<string | null>(null)
29+
const [isRegistered, setIsRegistered] = useState<boolean>(false)
30+
31+
const checkRegistrationStatus = useCallback(async (): Promise<void> => {
32+
if (!address) return
33+
34+
try {
35+
const registered = await checkUserRegistration(address)
36+
setIsRegistered(registered)
37+
} catch (error) {
38+
console.error('Error checking registration:', error)
39+
}
40+
}, [address])
2841

2942
const loadApps = useCallback(async () => {
3043
if (!address) return
@@ -64,7 +77,8 @@ export function MyApps(): React.JSX.Element {
6477
useEffect(() => {
6578
void loadApps()
6679
void loadTemplates()
67-
}, [loadApps, loadTemplates])
80+
void checkRegistrationStatus()
81+
}, [loadApps, loadTemplates, checkRegistrationStatus])
6882

6983
const handleDeleteApp = async (appId: number): Promise<void> => {
7084
if (!address || isDeleting !== null) return
@@ -105,16 +119,13 @@ export function MyApps(): React.JSX.Element {
105119
<div className="flex-grow-1 text-center">
106120
<h1 className="mb-0">My Apps</h1>
107121
</div>
108-
<Button
109-
variant="primary"
122+
<NewActionButton
123+
isRegistered={isRegistered}
110124
onClick={() => {
111125
setShowCreateModal(true)
112126
}}
113-
className="btn-sm d-flex align-items-center gap-2"
114-
>
115-
<i className="bi bi-plus-circle d-flex align-items-center"></i>
116-
New App
117-
</Button>
127+
label="New App"
128+
/>
118129
</div>
119130
<p className="text-center text-muted">Create and manage your Web4 applications. Build something amazing today.</p>
120131

src/pages/MyTemplates.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { useState, useEffect, useCallback } from 'react'
22
import { useAccount, useSignMessage } from 'wagmi'
3-
import { Alert, Button } from 'react-bootstrap'
4-
import { createTemplate, getMyTemplates, deleteTemplate } from '../services/api'
3+
import { Alert } from 'react-bootstrap'
4+
import { createTemplate, getMyTemplates, deleteTemplate, checkUserRegistration } from '../services/api'
55
import type { Template } from '../services/api'
66
import TemplateList from '../components/TemplateList'
77
import { Pagination } from '../components/Pagination'
88
import { CreateTemplateModal } from '../components/CreateTemplateModal'
99
import { DeleteTemplateModal } from '../components/DeleteTemplateModal'
10+
import { NewActionButton } from '../components/NewActionButton'
1011

1112
// Constants
1213
const ITEMS_PER_PAGE = 12
@@ -49,6 +50,18 @@ export function MyTemplates(): React.JSX.Element {
4950
const [templateToDelete, setTemplateToDelete] = useState<number | null>(null)
5051
const [currentPage, setCurrentPage] = useState(1)
5152
const [totalPages, setTotalPages] = useState(1)
53+
const [isRegistered, setIsRegistered] = useState<boolean>(false)
54+
55+
const checkRegistrationStatus = useCallback(async (): Promise<void> => {
56+
if (!address) return
57+
58+
try {
59+
const registered = await checkUserRegistration(address)
60+
setIsRegistered(registered)
61+
} catch (error) {
62+
console.error('Error checking registration:', error)
63+
}
64+
}, [address])
5265

5366
const loadTemplates = useCallback(async () => {
5467
if (!address) return
@@ -66,7 +79,8 @@ export function MyTemplates(): React.JSX.Element {
6679

6780
useEffect(() => {
6881
void loadTemplates()
69-
}, [loadTemplates])
82+
void checkRegistrationStatus()
83+
}, [loadTemplates, checkRegistrationStatus])
7084

7185
const validateForm = useCallback(() => {
7286
const newErrors: FormErrors = {
@@ -234,16 +248,13 @@ export function MyTemplates(): React.JSX.Element {
234248
<div className="flex-grow-1 text-center">
235249
<h1 className="mb-0">My Templates</h1>
236250
</div>
237-
<Button
238-
variant="primary"
251+
<NewActionButton
252+
isRegistered={isRegistered}
239253
onClick={() => {
240254
setShowCreateModal(true)
241255
}}
242-
className="btn-sm d-flex align-items-center gap-2"
243-
>
244-
<i className="bi bi-plus-circle d-flex align-items-center"></i>
245-
New Template
246-
</Button>
256+
label="New Template"
257+
/>
247258
</div>
248259
<p className="text-center text-muted">
249260
Create and manage your app templates. Share your innovations with the community.

src/pages/__tests__/MyApps.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,30 @@ describe('MyApps Component', () => {
8484
})
8585
})
8686

87+
it('disables New App button when user is not registered', async () => {
88+
// Mock user as not registered
89+
;(api.checkUserRegistration as Mock).mockResolvedValue(false)
90+
91+
render(<MyApps />)
92+
93+
await waitFor(() => {
94+
const newAppButton = screen.getByRole('button', { name: /New App/i })
95+
expect(newAppButton).toBeDisabled()
96+
})
97+
})
98+
99+
it('enables New App button when user is registered', async () => {
100+
// Mock user as registered
101+
;(api.checkUserRegistration as Mock).mockResolvedValue(true)
102+
103+
render(<MyApps />)
104+
105+
await waitFor(() => {
106+
const newAppButton = screen.getByRole('button', { name: /New App/i })
107+
expect(newAppButton).not.toBeDisabled()
108+
})
109+
})
110+
87111
// Remove the app deletion test since delete buttons are now only on view pages
88112
// We'll test deletion functionality in the ViewApp component tests
89113
})
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, it, vi, expect, beforeEach } from 'vitest'
2+
import { screen, waitFor } from '@testing-library/react'
3+
import { customRender as render } from '../../test/test-utils-helpers'
4+
import { MyTemplates } from '../MyTemplates'
5+
import * as wagmi from 'wagmi'
6+
import * as api from '../../services/api'
7+
import type { Mock } from 'vitest'
8+
9+
// Mock wagmi hooks
10+
vi.mock('wagmi', () => ({
11+
useAccount: vi.fn(),
12+
useSignMessage: vi.fn(),
13+
WagmiProvider: ({ children }: { children: React.ReactNode }) => children,
14+
}))
15+
16+
// Mock API services
17+
vi.mock('../../services/api', () => ({
18+
checkUserRegistration: vi.fn(),
19+
getMyTemplates: vi.fn(),
20+
deleteTemplate: vi.fn(),
21+
createTemplate: vi.fn(),
22+
}))
23+
24+
describe('MyTemplates Component', () => {
25+
const mockAddress = '0x123...'
26+
const mockSignMessage = vi.fn()
27+
28+
beforeEach(() => {
29+
vi.clearAllMocks()
30+
31+
// Default mock implementations
32+
;(wagmi.useAccount as Mock).mockReturnValue({
33+
address: mockAddress,
34+
isConnected: true,
35+
})
36+
;(wagmi.useSignMessage as Mock).mockReturnValue({
37+
signMessageAsync: mockSignMessage,
38+
})
39+
;(api.checkUserRegistration as Mock).mockResolvedValue(true)
40+
;(api.getMyTemplates as Mock).mockResolvedValue([])
41+
})
42+
43+
it('displays templates when loaded', async () => {
44+
const mockTemplates = [
45+
{
46+
id: 1,
47+
title: 'Test Template',
48+
description: 'Test Description',
49+
url: 'https://example.com',
50+
data: '{}',
51+
created_at: new Date().toISOString(),
52+
owner_address: mockAddress,
53+
updated_at: new Date().toISOString(),
54+
},
55+
]
56+
57+
;(api.getMyTemplates as Mock).mockResolvedValue(mockTemplates)
58+
59+
render(<MyTemplates />)
60+
61+
await waitFor(() => {
62+
expect(screen.getByText('Test Template')).toBeInTheDocument()
63+
expect(screen.getByText('Test Description')).toBeInTheDocument()
64+
})
65+
})
66+
67+
it('disables New Template button when user is not registered', async () => {
68+
// Mock user as not registered
69+
;(api.checkUserRegistration as Mock).mockResolvedValue(false)
70+
71+
render(<MyTemplates />)
72+
73+
await waitFor(() => {
74+
const newTemplateButton = screen.getByRole('button', { name: /New Template/i })
75+
expect(newTemplateButton).toBeDisabled()
76+
})
77+
})
78+
79+
it('enables New Template button when user is registered', async () => {
80+
// Mock user as registered
81+
;(api.checkUserRegistration as Mock).mockResolvedValue(true)
82+
83+
render(<MyTemplates />)
84+
85+
await waitFor(() => {
86+
const newTemplateButton = screen.getByRole('button', { name: /New Template/i })
87+
expect(newTemplateButton).not.toBeDisabled()
88+
})
89+
})
90+
})

0 commit comments

Comments
 (0)