Skip to content

Commit aec8a68

Browse files
committed
feat: login page
1 parent efebf08 commit aec8a68

5 files changed

Lines changed: 687 additions & 0 deletions

File tree

.storybook/public/logo.png

4.31 KB
Loading
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import React from 'react'
2+
import type { Meta, StoryObj } from '@storybook/react'
3+
import { action } from '@storybook/addon-actions'
4+
import { Login } from './Login'
5+
6+
const meta: Meta<typeof Login> = {
7+
title: 'Foundary/Login',
8+
component: Login,
9+
parameters: {
10+
layout: 'fullscreen',
11+
docs: {
12+
description: {
13+
component:
14+
'A highly customizable and elegant login component with configurable text, branding, logo, and workspace choices. ' +
15+
'Features a single-click login, loading states, smooth animations, and integrated brand logo. ' +
16+
'All text content and post-login workspace options can be customized via props. ' +
17+
'Workspace choices support internal URLs, external URLs, and custom callback functions for flexible navigation.'
18+
}
19+
}
20+
},
21+
argTypes: {
22+
onLogin: {
23+
action: 'login clicked',
24+
description: 'Callback function called when login button is clicked'
25+
},
26+
isLoading: {
27+
control: 'boolean',
28+
description: 'Shows loading state when login is being processed externally'
29+
},
30+
className: {
31+
control: 'text',
32+
description: 'Additional CSS classes for the login container'
33+
},
34+
title: {
35+
control: 'text',
36+
description: 'Main title text for the login screen'
37+
},
38+
subtitle: {
39+
control: 'text',
40+
description: 'Subtitle/description text for the login screen'
41+
},
42+
buttonText: {
43+
control: 'text',
44+
description: 'Text for the login button'
45+
},
46+
loadingText: {
47+
control: 'text',
48+
description: 'Loading text shown when signing in'
49+
},
50+
welcomeTitle: {
51+
control: 'text',
52+
description: 'Post-login welcome title'
53+
},
54+
welcomeSubtitle: {
55+
control: 'text',
56+
description: 'Post-login subtitle for workspace selection'
57+
},
58+
logoSrc: {
59+
control: 'text',
60+
description: 'Logo source URL'
61+
},
62+
logoAlt: {
63+
control: 'text',
64+
description: 'Logo alt text'
65+
},
66+
workspaceChoices: {
67+
control: 'object',
68+
description: 'Array of workspace choice options for post-login selection'
69+
}
70+
},
71+
tags: ['autodocs']
72+
}
73+
74+
export default meta
75+
type Story = StoryObj<typeof Login>
76+
77+
// Mock login handler for stories
78+
const mockLoginHandler = () => {
79+
action('Login button clicked')()
80+
}
81+
82+
/**
83+
* Default login with simple button
84+
*/
85+
export const Default: Story = {
86+
args: {
87+
onLogin: mockLoginHandler,
88+
isLoading: false
89+
},
90+
parameters: {
91+
docs: {
92+
description: {
93+
story: 'The default login with a simple button. Click to see the post-login workspace selection.'
94+
}
95+
}
96+
}
97+
}
98+
99+
/**
100+
* Login in external loading state
101+
*/
102+
export const Loading: Story = {
103+
args: {
104+
onLogin: mockLoginHandler,
105+
isLoading: true
106+
},
107+
parameters: {
108+
docs: {
109+
description: {
110+
story: 'Login with external loading state. The button is disabled and shows loading indicators.'
111+
}
112+
}
113+
}
114+
}
115+
116+
/**
117+
* Login with custom styling
118+
*/
119+
export const CustomStyling: Story = {
120+
args: {
121+
onLogin: mockLoginHandler,
122+
className: 'bg-gradient-to-br from-indigo-50 to-cyan-50',
123+
isLoading: false
124+
},
125+
parameters: {
126+
docs: {
127+
description: {
128+
story: 'Login with custom background gradient styling.'
129+
}
130+
}
131+
}
132+
}
133+
134+
/**
135+
* Customized text and branding example
136+
*/
137+
export const CustomBranding: Story = {
138+
args: {
139+
onLogin: mockLoginHandler,
140+
title: "Welcome to Acme Corp",
141+
subtitle: "Access your enterprise workspace",
142+
buttonText: "Enter System",
143+
loadingText: "Authenticating...",
144+
welcomeTitle: "Hello, Welcome!",
145+
welcomeSubtitle: "Select your preferred working mode",
146+
logoAlt: "Acme Corporation Logo",
147+
isLoading: false
148+
},
149+
parameters: {
150+
docs: {
151+
description: {
152+
story: 'Example showing how to customize all text content and branding for different organizations.'
153+
}
154+
}
155+
}
156+
}
157+
158+
/**
159+
* Custom workspace choices with different options
160+
*/
161+
export const CustomWorkspaceChoices: Story = {
162+
args: {
163+
onLogin: mockLoginHandler,
164+
workspaceChoices: [
165+
{
166+
id: 'analytics',
167+
title: 'Analytics Dashboard',
168+
description: 'View reports, metrics, and business intelligence data.',
169+
icon: React.createElement('div', { className: 'h-12 w-12 bg-green-500 rounded-lg flex items-center justify-center text-white font-bold text-xl' }, 'A'),
170+
color: 'text-green-600',
171+
gradient: 'from-green-500 to-teal-600',
172+
url: '/dashboard/analytics'
173+
},
174+
{
175+
id: 'admin',
176+
title: 'Admin Panel',
177+
description: 'Manage users, settings, and system configuration.',
178+
icon: React.createElement('div', { className: 'h-12 w-12 bg-red-500 rounded-lg flex items-center justify-center text-white font-bold text-xl' }, 'S'),
179+
color: 'text-red-600',
180+
gradient: 'from-red-500 to-pink-600',
181+
url: '/admin'
182+
},
183+
{
184+
id: 'external',
185+
title: 'External Tool',
186+
description: 'Launch external application in new tab.',
187+
icon: React.createElement('div', { className: 'h-12 w-12 bg-blue-500 rounded-lg flex items-center justify-center text-white font-bold text-xl' }, 'E'),
188+
color: 'text-blue-600',
189+
gradient: 'from-blue-500 to-indigo-600',
190+
url: 'https://example.com'
191+
},
192+
{
193+
id: 'custom',
194+
title: 'Custom Action',
195+
description: 'Execute custom callback function.',
196+
icon: React.createElement('div', { className: 'h-12 w-12 bg-purple-500 rounded-lg flex items-center justify-center text-white font-bold text-xl' }, 'C'),
197+
color: 'text-purple-600',
198+
gradient: 'from-purple-500 to-violet-600',
199+
onClick: () => action('Custom workspace action executed')()
200+
}
201+
],
202+
isLoading: false
203+
},
204+
parameters: {
205+
docs: {
206+
description: {
207+
story:
208+
'Example showing custom workspace choices with different navigation options: ' +
209+
'internal URLs, external URLs, and custom callback functions.'
210+
}
211+
}
212+
}
213+
}
214+
215+
/**
216+
* Interactive demo showing complete user flow
217+
*/
218+
export const InteractiveDemo: Story = {
219+
args: {
220+
onLogin: () => {
221+
action('Interactive demo - User clicked login')()
222+
},
223+
isLoading: false
224+
},
225+
parameters: {
226+
docs: {
227+
description: {
228+
story:
229+
'Interactive demo showing the complete login flow. Click the login button to see the ' +
230+
'workspace selection screen with Ava (digital assistant) and team collaboration options.'
231+
}
232+
}
233+
}
234+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { describe, it, expect, vi } from 'vitest'
2+
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
3+
import { Login } from './Login'
4+
5+
describe('Login Component', () => {
6+
it('renders the login button and logo with default props', () => {
7+
render(<Login />)
8+
9+
expect(screen.getByRole('button', { name: /sign in to continue/i })).toBeTruthy()
10+
expect(screen.getByText('Welcome to Cortex')).toBeTruthy()
11+
expect(screen.getByText('Click below to access your digital workspace')).toBeTruthy()
12+
expect(screen.getByAltText('Cortex Logo')).toBeTruthy()
13+
})
14+
15+
it('shows loading state when isLoading prop is true', () => {
16+
render(<Login isLoading={true} />)
17+
18+
const button = screen.getByRole('button')
19+
expect(button.hasAttribute('disabled')).toBe(true)
20+
expect(screen.getByText('Signing you in...')).toBeTruthy()
21+
})
22+
23+
it('calls onLogin callback when button is clicked', async () => {
24+
const mockOnLogin = vi.fn()
25+
render(<Login onLogin={mockOnLogin} />)
26+
27+
const button = screen.getByRole('button', { name: /sign in to continue/i })
28+
fireEvent.click(button)
29+
30+
// Wait for the simulated login process to complete
31+
await waitFor(() => {
32+
expect(mockOnLogin).toHaveBeenCalled()
33+
}, { timeout: 2000 })
34+
})
35+
36+
it('shows workspace selection after login', async () => {
37+
render(<Login />)
38+
39+
const button = screen.getByRole('button', { name: /sign in to continue/i })
40+
fireEvent.click(button)
41+
42+
// Wait for the login process and workspace selection to appear
43+
await waitFor(() => {
44+
expect(screen.getByText('Choose how you\'d like to get started')).toBeTruthy()
45+
}, { timeout: 2000 })
46+
47+
// Check that both workspace options are available
48+
expect(screen.getByText('Chat with Ava')).toBeTruthy()
49+
expect(screen.getByText('Collaborate with Team')).toBeTruthy()
50+
51+
// Verify logo is present in both login and post-login states
52+
expect(screen.getAllByAltText('Cortex Logo')).toHaveLength(2)
53+
})
54+
55+
it('applies custom className', () => {
56+
const { container } = render(<Login className="custom-class" />)
57+
58+
expect((container.firstChild as HTMLElement)?.className).toContain('custom-class')
59+
})
60+
61+
it('shows loading state during login process', async () => {
62+
render(<Login />)
63+
64+
const button = screen.getByRole('button', { name: /sign in to continue/i })
65+
fireEvent.click(button)
66+
67+
// Should show loading state immediately after click
68+
expect(screen.getByText('Signing you in...')).toBeTruthy()
69+
expect(button.hasAttribute('disabled')).toBe(true)
70+
})
71+
72+
it('renders custom workspace choices', async () => {
73+
const customChoices = [
74+
{
75+
id: 'custom1',
76+
title: 'Custom Option',
77+
description: 'A custom workspace option',
78+
icon: <div>Icon</div>,
79+
color: 'text-blue-600',
80+
gradient: 'from-blue-500 to-green-600',
81+
url: '/custom'
82+
}
83+
]
84+
85+
render(<Login workspaceChoices={customChoices} />)
86+
87+
const button = screen.getByRole('button', { name: /sign in to continue/i })
88+
fireEvent.click(button)
89+
90+
// Wait for the workspace selection to appear
91+
await waitFor(() => {
92+
expect(screen.getByText('Custom Option')).toBeTruthy()
93+
expect(screen.getByText('A custom workspace option')).toBeTruthy()
94+
}, { timeout: 2000 })
95+
})
96+
97+
it('handles workspace choice with custom onClick', async () => {
98+
const mockOnClick = vi.fn()
99+
const customChoices = [
100+
{
101+
id: 'custom1',
102+
title: 'Custom Option',
103+
description: 'A custom workspace option',
104+
icon: <div>Icon</div>,
105+
color: 'text-blue-600',
106+
gradient: 'from-blue-500 to-green-600',
107+
onClick: mockOnClick
108+
}
109+
]
110+
111+
render(<Login workspaceChoices={customChoices} />)
112+
113+
const loginButton = screen.getByRole('button', { name: /sign in to continue/i })
114+
fireEvent.click(loginButton)
115+
116+
// Wait for the workspace selection to appear
117+
await waitFor(() => {
118+
expect(screen.getByText('Custom Option')).toBeTruthy()
119+
}, { timeout: 2000 })
120+
121+
// Click the workspace choice
122+
const workspaceCard = screen.getByText('Custom Option').closest('div')
123+
if (workspaceCard) {
124+
fireEvent.click(workspaceCard)
125+
expect(mockOnClick).toHaveBeenCalled()
126+
}
127+
})
128+
})
129+
130+
it('renders with custom props', () => {
131+
const customProps = {
132+
title: 'Custom Title',
133+
subtitle: 'Custom Subtitle',
134+
buttonText: 'Custom Button',
135+
logoAlt: 'Custom Logo'
136+
}
137+
138+
render(<Login {...customProps} />)
139+
140+
expect(screen.getByText('Custom Title')).toBeTruthy()
141+
expect(screen.getByText('Custom Subtitle')).toBeTruthy()
142+
expect(screen.getByRole('button', { name: /custom button/i })).toBeTruthy()
143+
expect(screen.getByAltText('Custom Logo')).toBeTruthy()
144+
})

0 commit comments

Comments
 (0)