@liiift-studio/sales-portal 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +461 -0
- package/SETUP.md +230 -0
- package/api/getAnalytics.d.ts +38 -0
- package/api/getAnalytics.js +346 -0
- package/api/getBalanceTransactions.d.ts +29 -0
- package/api/getBalanceTransactions.js +125 -0
- package/api/getDesignerInfo.d.ts +37 -0
- package/api/getDesignerInfo.js +98 -0
- package/api/getDesigners.d.ts +28 -0
- package/api/getDesigners.js +63 -0
- package/api/getPreviousSales.d.ts +23 -0
- package/api/getPreviousSales.js +82 -0
- package/api/getSales.d.ts +29 -0
- package/api/getSales.js +50 -0
- package/api/getSalesRange.d.ts +23 -0
- package/api/getSalesRange.js +58 -0
- package/api/utils/authMiddleware.js +84 -0
- package/api/utils/dateUtils.js +69 -0
- package/api/utils/feeCalculator.js +148 -0
- package/api/utils/processors/invoiceProcessor.js +337 -0
- package/api/utils/processors/paymentProcessor.js +462 -0
- package/api/utils/salesDataProcessing.js +596 -0
- package/api/utils/salesDataProcessor.js +224 -0
- package/api/utils/stripeFetcher.js +248 -0
- package/components/DateRangeSalesTable.js +1072 -0
- package/components/DebugValues.js +48 -0
- package/components/LicenseTypeList.js +193 -0
- package/components/LoginForm.js +219 -0
- package/components/PeriodComparison.js +501 -0
- package/components/Sales.js +773 -0
- package/components/SalesChart.js +307 -0
- package/components/SalesPortalPage.js +147 -0
- package/components/SalesTable.js +677 -0
- package/components/SummaryCards.js +345 -0
- package/components/TopPerformers.js +331 -0
- package/components/TypefaceList.js +154 -0
- package/components/table-columns.js +70 -0
- package/components/table-row-cells.js +295 -0
- package/data/countryCode.json +318 -0
- package/hooks/useSalesDateQuery.d.ts +20 -0
- package/hooks/useSalesDateQuery.js +71 -0
- package/index.d.ts +172 -0
- package/index.js +33 -0
- package/package.json +87 -0
- package/styles/sales-portal.module.scss +383 -0
- package/styles/sales-portal.theme.d.ts +5 -0
- package/styles/sales-portal.theme.js +799 -0
- package/utils/currencyUtils.d.ts +20 -0
- package/utils/currencyUtils.js +79 -0
- package/utils/salesDataProcessing.d.ts +44 -0
- package/utils/salesDataProcessing.js +596 -0
- package/utils/useSalesDateQuery.js +71 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Debug component to help troubleshoot value issues
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Typography, Paper } from '@mui/material';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Debug component to display raw and processed values
|
|
7
|
+
* This is a temporary component to help track data values and transformations
|
|
8
|
+
*/
|
|
9
|
+
export default function DebugValues({
|
|
10
|
+
totalRevenue,
|
|
11
|
+
previousRevenue,
|
|
12
|
+
revenueChange,
|
|
13
|
+
chartState
|
|
14
|
+
}) {
|
|
15
|
+
// Format as currency without dividing
|
|
16
|
+
const formatRawCurrency = (value) => {
|
|
17
|
+
return new Intl.NumberFormat('en-US', {
|
|
18
|
+
style: 'currency',
|
|
19
|
+
currency: 'USD',
|
|
20
|
+
minimumFractionDigits: 2,
|
|
21
|
+
maximumFractionDigits: 2,
|
|
22
|
+
}).format(value);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Calculate the percentage manually
|
|
26
|
+
const calculatePercentage = (current, previous) => {
|
|
27
|
+
if (!previous) return 'N/A';
|
|
28
|
+
const change = ((current - previous) / previous) * 100;
|
|
29
|
+
return change.toFixed(1) + '%';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<Paper elevation={1} sx={{ p: 2, m: 2, bgcolor: '#f8f9fa', display: 'none' }}>
|
|
34
|
+
<Typography variant="h6">Debug Values</Typography>
|
|
35
|
+
|
|
36
|
+
<Box sx={{ mt: 2 }}>
|
|
37
|
+
<Typography variant="body2">Raw Total Revenue: {formatRawCurrency(totalRevenue)}</Typography>
|
|
38
|
+
<Typography variant="body2">Raw Previous Revenue: {formatRawCurrency(previousRevenue)}</Typography>
|
|
39
|
+
<Typography variant="body2">Revenue Change (calc): {calculatePercentage(totalRevenue, previousRevenue)}</Typography>
|
|
40
|
+
<Typography variant="body2">Revenue Change (state): {revenueChange?.toFixed(1)}%</Typography>
|
|
41
|
+
|
|
42
|
+
<Typography variant="body2" sx={{ mt: 1 }}>Chart Net Total: {formatRawCurrency(chartState?.salesMax)}</Typography>
|
|
43
|
+
<Typography variant="body2">Chart Tax Total: {formatRawCurrency(chartState?.taxData?.at?.(-1) || 0)}</Typography>
|
|
44
|
+
<Typography variant="body2">Chart Shipping Total: {formatRawCurrency(chartState?.shippingData?.at?.(-1) || 0)}</Typography>
|
|
45
|
+
</Box>
|
|
46
|
+
</Paper>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// License type list component displaying all license types and their sales data with percentage sorting capability
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { Grid, Typography, Box, Tooltip, FormControl, InputLabel, Select, MenuItem, Stack } from '@mui/material';
|
|
4
|
+
import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward';
|
|
5
|
+
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
|
6
|
+
import styles from '../styles/sales-portal.module.scss';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* List of license types with their sales information
|
|
10
|
+
* Supports sorting by revenue, license name, order count, and percentage of total revenue
|
|
11
|
+
*/
|
|
12
|
+
export default function LicenseTypeList({ licenseTypeData, sales, loading, admin }) {
|
|
13
|
+
const [sortKey, setSortKey] = useState('percentage'); // Default sort is by percentage
|
|
14
|
+
const [sortDirection, setSortDirection] = useState('desc'); // Default direction is descending (highest first)
|
|
15
|
+
|
|
16
|
+
// Calculate total revenue for percentage calculations
|
|
17
|
+
const totalRevenue = React.useMemo(() => {
|
|
18
|
+
if (!licenseTypeData) return 0;
|
|
19
|
+
return licenseTypeData.reduce((sum, l) => sum + (l.grossTotal - l.taxTotal - l.refundTotal), 0);
|
|
20
|
+
}, [licenseTypeData]);
|
|
21
|
+
|
|
22
|
+
// Sort license type data based on current sort configuration
|
|
23
|
+
const sortedLicenseTypeData = React.useMemo(() => {
|
|
24
|
+
if (!licenseTypeData) return [];
|
|
25
|
+
|
|
26
|
+
const sortableData = [...licenseTypeData];
|
|
27
|
+
|
|
28
|
+
sortableData.sort((a, b) => {
|
|
29
|
+
if (sortKey === 'revenue') {
|
|
30
|
+
// Sort by revenue (gross total minus tax and refunds)
|
|
31
|
+
const valueA = a.grossTotal - a.taxTotal - a.refundTotal;
|
|
32
|
+
const valueB = b.grossTotal - b.taxTotal - b.refundTotal;
|
|
33
|
+
return sortDirection === 'asc' ? valueA - valueB : valueB - valueA;
|
|
34
|
+
}
|
|
35
|
+
else if (sortKey === 'orders') {
|
|
36
|
+
// Sort by number of orders
|
|
37
|
+
return sortDirection === 'asc' ? a.orders - b.orders : b.orders - a.orders;
|
|
38
|
+
}
|
|
39
|
+
else if (sortKey === 'percentage') {
|
|
40
|
+
// Sort by percentage of total revenue
|
|
41
|
+
const valueA = (a.grossTotal - a.taxTotal - a.refundTotal) / totalRevenue;
|
|
42
|
+
const valueB = (b.grossTotal - b.taxTotal - b.refundTotal) / totalRevenue;
|
|
43
|
+
return sortDirection === 'asc' ? valueA - valueB : valueB - valueA;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Sort alphabetically by license name
|
|
47
|
+
return sortDirection === 'asc'
|
|
48
|
+
? a.name.localeCompare(b.name)
|
|
49
|
+
: b.name.localeCompare(a.name);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return sortableData;
|
|
54
|
+
}, [licenseTypeData, sortKey, sortDirection]);
|
|
55
|
+
|
|
56
|
+
// Toggle sort direction
|
|
57
|
+
const toggleSortDirection = () => {
|
|
58
|
+
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<Grid container className='license-type-section'>
|
|
63
|
+
<Grid
|
|
64
|
+
item
|
|
65
|
+
xs={12}
|
|
66
|
+
data-disabled={loading}
|
|
67
|
+
data-loading={loading}
|
|
68
|
+
sx={{
|
|
69
|
+
margin: "20px 0 10px",
|
|
70
|
+
borderRadius: "4px",
|
|
71
|
+
flex: "none",
|
|
72
|
+
opacity: !!sales.length ? 1 : 0.25,
|
|
73
|
+
pointerEvents: !!sales.length ? "" : "none",
|
|
74
|
+
border: "1px solid var(--black, black)",
|
|
75
|
+
'&:hover': { boxShadow: "0 0 8px rgba(0,0,0,0.25)" },
|
|
76
|
+
}}
|
|
77
|
+
>
|
|
78
|
+
<Box sx={{
|
|
79
|
+
background: "rgba(var(--blackRGB, 0,0,0), 1)",
|
|
80
|
+
color: "var(--white, white)",
|
|
81
|
+
padding: "5px 10px 10px",
|
|
82
|
+
display: "flex",
|
|
83
|
+
justifyContent: "space-between",
|
|
84
|
+
alignItems: "center"
|
|
85
|
+
}}>
|
|
86
|
+
<Typography variant='h5'>
|
|
87
|
+
<strong>License Type</strong>
|
|
88
|
+
</Typography>
|
|
89
|
+
<Stack direction="row" spacing={2} alignItems="center">
|
|
90
|
+
<FormControl variant="filled" size="small" sx={{
|
|
91
|
+
minWidth: 120,
|
|
92
|
+
'& .MuiFilledInput-root': { color: "var(--white, white)"},
|
|
93
|
+
'& .MuiFormLabel-root': { color: 'rgba(255,255,255,0.7)' },
|
|
94
|
+
'& .MuiSelect-icon': { display: 'none' },
|
|
95
|
+
}}>
|
|
96
|
+
<InputLabel id="license-sort-label">Sort by</InputLabel>
|
|
97
|
+
<Select
|
|
98
|
+
labelId="license-sort-label"
|
|
99
|
+
value={sortKey}
|
|
100
|
+
onChange={(e) => setSortKey(e.target.value)}
|
|
101
|
+
label="Sort by"
|
|
102
|
+
>
|
|
103
|
+
<MenuItem value="revenue">Revenue</MenuItem>
|
|
104
|
+
<MenuItem value="percentage">Percentage</MenuItem>
|
|
105
|
+
<MenuItem value="name">Name</MenuItem>
|
|
106
|
+
<MenuItem value="orders">Orders</MenuItem>
|
|
107
|
+
</Select>
|
|
108
|
+
</FormControl>
|
|
109
|
+
<Tooltip title={`Sort ${sortDirection === 'asc' ? 'Descending' : 'Ascending'}`}>
|
|
110
|
+
<Box
|
|
111
|
+
onClick={toggleSortDirection}
|
|
112
|
+
sx={{
|
|
113
|
+
display: { xs: 'none', sm: 'flex' },
|
|
114
|
+
cursor: 'pointer',
|
|
115
|
+
alignItems: 'center',
|
|
116
|
+
justifyContent: 'center',
|
|
117
|
+
padding: '6px 10px',
|
|
118
|
+
color: "var(--white, white)",
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{sortDirection === 'asc' ? <ArrowUpwardIcon /> : <ArrowDownwardIcon />}
|
|
122
|
+
</Box>
|
|
123
|
+
</Tooltip>
|
|
124
|
+
</Stack>
|
|
125
|
+
</Box>
|
|
126
|
+
<Box sx={{
|
|
127
|
+
background: "rgba(var(--blackRGB, 0,0,0), 0.06)",
|
|
128
|
+
padding: "10px",
|
|
129
|
+
pb: 8
|
|
130
|
+
}}>
|
|
131
|
+
{sortedLicenseTypeData?.map((license, i) => {
|
|
132
|
+
// Calculate the revenue
|
|
133
|
+
const revenue = license?.grossTotal - license?.taxTotal - license?.refundTotal;
|
|
134
|
+
// Calculate percentage
|
|
135
|
+
const percentage = (revenue / totalRevenue) * 100;
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Typography
|
|
139
|
+
key={`license-total-${i}`}
|
|
140
|
+
variant='h5'
|
|
141
|
+
sx={{
|
|
142
|
+
color: 'var(--black, black)',
|
|
143
|
+
whiteSpace: "nowrap",
|
|
144
|
+
overflow: "hidden",
|
|
145
|
+
textOverflow: "ellipsis",
|
|
146
|
+
paddingTop: licenseTypeData[i-1] && license.name[0] !== licenseTypeData[i-1].name[0] ? "0.5em" : "",
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{sortKey === 'percentage' ? (
|
|
150
|
+
// When sorted by percentage, show percentage first
|
|
151
|
+
<>
|
|
152
|
+
<span className={styles.earningContainer}>
|
|
153
|
+
<strong style={{minWidth: "100px", display: "inline-block", textAlign: "right"}}>
|
|
154
|
+
{percentage.toFixed(1)}%
|
|
155
|
+
</strong>
|
|
156
|
+
</span>
|
|
157
|
+
<span> — </span>
|
|
158
|
+
<span>
|
|
159
|
+
{license.name}
|
|
160
|
+
<span style={{opacity:"0.5", textTransform: "none"}}>({license.orders} Orders)</span>
|
|
161
|
+
<span style={{opacity:"0.5", marginLeft: "10px"}}>
|
|
162
|
+
<span style={{opacity: "0.5"}}>USD</span>$
|
|
163
|
+
{(revenue / 100).toLocaleString('en-US', {minimumFractionDigits: 2})}
|
|
164
|
+
</span>
|
|
165
|
+
</span>
|
|
166
|
+
</>
|
|
167
|
+
) : (
|
|
168
|
+
// Default display (revenue first)
|
|
169
|
+
<>
|
|
170
|
+
<span className={styles.earningContainer}>
|
|
171
|
+
<span style={{opacity: "0.5"}}>USD</span>$
|
|
172
|
+
<strong style={{minWidth: "100px", display: "inline-block", textAlign: "right"}}>
|
|
173
|
+
{(revenue / 100).toLocaleString('en-US', {minimumFractionDigits: 2})}
|
|
174
|
+
</strong>
|
|
175
|
+
</span>
|
|
176
|
+
<span> — </span>
|
|
177
|
+
<span>
|
|
178
|
+
{license.name}
|
|
179
|
+
<span style={{opacity:"0.5", textTransform: "none"}}>({license.orders} Orders)</span>
|
|
180
|
+
<span style={{opacity:"0.5", marginLeft: "10px"}}>
|
|
181
|
+
{percentage.toFixed(1)}% of total
|
|
182
|
+
</span>
|
|
183
|
+
</span>
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
</Typography>
|
|
187
|
+
);
|
|
188
|
+
})}
|
|
189
|
+
</Box>
|
|
190
|
+
</Grid>
|
|
191
|
+
</Grid>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Login form component for sales portal authentication
|
|
2
|
+
import React, { useEffect, useState } from 'react';
|
|
3
|
+
import { Grid, Typography, Input, Button, Box } from '@mui/material';
|
|
4
|
+
import packageJson from '../package.json';
|
|
5
|
+
|
|
6
|
+
const { version } = packageJson;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* LoginForm component for designer authentication
|
|
10
|
+
* @param {Object} props Component props
|
|
11
|
+
* @param {Function} props.onLoginSuccess Callback when login succeeds, receives { designers, admin }
|
|
12
|
+
* @param {Function} props.renderLoader Optional render prop for custom loader component
|
|
13
|
+
* @param {string} props.apiEndpoint Optional custom API endpoint (default: '/api/sales-portal/getDesignerInfo')
|
|
14
|
+
* @param {Object} props.texts Optional text customization { title, description, subtitle, buttonLabel }
|
|
15
|
+
*/
|
|
16
|
+
export default function LoginForm({
|
|
17
|
+
onLoginSuccess,
|
|
18
|
+
renderLoader,
|
|
19
|
+
apiEndpoint = '/api/sales-portal/getDesignerInfo',
|
|
20
|
+
texts = {}
|
|
21
|
+
}) {
|
|
22
|
+
const [user, setUser] = useState('');
|
|
23
|
+
const [password, setPassword] = useState('');
|
|
24
|
+
const [message, setMessage] = useState('');
|
|
25
|
+
const [loading, setLoading] = useState(false);
|
|
26
|
+
|
|
27
|
+
const {
|
|
28
|
+
title = 'Sales Portal',
|
|
29
|
+
description = 'A comprehensive dashboard for type designers to track their typeface sales performance and revenue.',
|
|
30
|
+
subtitle = 'All sales are reviewed internally before payouts. Contact support if you have questions about your sales data.',
|
|
31
|
+
buttonLabel = 'Login',
|
|
32
|
+
errorMessage = 'Incorrect username/password or the user is not a designer.',
|
|
33
|
+
emptyFieldsMessage = 'Please enter a username and password.',
|
|
34
|
+
disabledMessage = 'Sales portal is currently disabled.'
|
|
35
|
+
} = texts;
|
|
36
|
+
|
|
37
|
+
const handleLogin = () => {
|
|
38
|
+
if (user !== '' && password !== '') {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
fetch(apiEndpoint, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ user, password }),
|
|
44
|
+
})
|
|
45
|
+
.then((res) => res.json())
|
|
46
|
+
.then((res) => {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
if (res.disabled) {
|
|
49
|
+
setMessage(disabledMessage);
|
|
50
|
+
} else if (res.success) {
|
|
51
|
+
setMessage('');
|
|
52
|
+
onLoginSuccess({
|
|
53
|
+
designers: res.data,
|
|
54
|
+
admin: res.admin || false,
|
|
55
|
+
user,
|
|
56
|
+
password
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
setMessage(res.message || errorMessage);
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.catch((err) => {
|
|
63
|
+
setMessage(err.message || errorMessage);
|
|
64
|
+
setLoading(false);
|
|
65
|
+
});
|
|
66
|
+
} else {
|
|
67
|
+
setMessage(emptyFieldsMessage);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Clear message when inputs change
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
setMessage('');
|
|
74
|
+
}, [password, user]);
|
|
75
|
+
|
|
76
|
+
// Handle Enter key press
|
|
77
|
+
const handleKeyPress = (e) => {
|
|
78
|
+
if (e.key === 'Enter') {
|
|
79
|
+
handleLogin();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<Grid container columnSpacing={10} data-loading={loading} sx={{
|
|
85
|
+
pb: 20,
|
|
86
|
+
position: 'relative',
|
|
87
|
+
display: 'flex',
|
|
88
|
+
alignContent: 'flex-start',
|
|
89
|
+
px: { xs: 'var(--marginXMobile)', md: 'var(--marginX)' },
|
|
90
|
+
minHeight: '80vh'
|
|
91
|
+
}}>
|
|
92
|
+
|
|
93
|
+
{loading && renderLoader && renderLoader({ fill: true, position: 'fixed', height: '100%' })}
|
|
94
|
+
|
|
95
|
+
<Grid id='titleContainer' item xs={12} sx={{ py: 10 }}>
|
|
96
|
+
<Typography component={'span'} variant='h2'>{title}</Typography>
|
|
97
|
+
</Grid>
|
|
98
|
+
|
|
99
|
+
<Grid item xs={12} sm="auto" sx={{ minWidth: "400px", zIndex: 1 }}>
|
|
100
|
+
<Input
|
|
101
|
+
placeholder='Email'
|
|
102
|
+
onChange={(e) => setUser(e.target.value)}
|
|
103
|
+
value={user}
|
|
104
|
+
type='email'
|
|
105
|
+
autoComplete="username"
|
|
106
|
+
onKeyPress={handleKeyPress}
|
|
107
|
+
sx={{
|
|
108
|
+
m: 0,
|
|
109
|
+
mt: 1,
|
|
110
|
+
border: 'none',
|
|
111
|
+
typography: 'body1',
|
|
112
|
+
color: 'var(--grey800)',
|
|
113
|
+
display: 'flex',
|
|
114
|
+
justifyContent: 'flex-start',
|
|
115
|
+
bgcolor: 'white',
|
|
116
|
+
borderBottom: "2px solid var(--black)",
|
|
117
|
+
maxWidth: '400px!important',
|
|
118
|
+
width: '100%',
|
|
119
|
+
minHeight: '60px',
|
|
120
|
+
'& input': {
|
|
121
|
+
pl: '15px',
|
|
122
|
+
height: '60px',
|
|
123
|
+
},
|
|
124
|
+
'& ::placeholder': {
|
|
125
|
+
color: 'var(--grey800)',
|
|
126
|
+
opacity: 1,
|
|
127
|
+
}
|
|
128
|
+
}}
|
|
129
|
+
/>
|
|
130
|
+
<Input
|
|
131
|
+
placeholder='Password'
|
|
132
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
133
|
+
value={password}
|
|
134
|
+
type='password'
|
|
135
|
+
autoComplete="current-password"
|
|
136
|
+
onKeyPress={handleKeyPress}
|
|
137
|
+
sx={{
|
|
138
|
+
m: 0,
|
|
139
|
+
border: 'none',
|
|
140
|
+
typography: 'body1',
|
|
141
|
+
color: 'var(--grey800)',
|
|
142
|
+
display: 'flex',
|
|
143
|
+
justifyContent: 'flex-start',
|
|
144
|
+
bgcolor: 'white',
|
|
145
|
+
borderBottom: "2px solid var(--black)",
|
|
146
|
+
maxWidth: '400px!important',
|
|
147
|
+
width: '100%',
|
|
148
|
+
minHeight: '60px',
|
|
149
|
+
'& input': {
|
|
150
|
+
pl: '15px',
|
|
151
|
+
height: '60px',
|
|
152
|
+
},
|
|
153
|
+
'& ::placeholder': {
|
|
154
|
+
color: 'var(--grey800)',
|
|
155
|
+
opacity: 1,
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
/>
|
|
159
|
+
<Button
|
|
160
|
+
variant="contained"
|
|
161
|
+
className="bold"
|
|
162
|
+
onClick={handleLogin}
|
|
163
|
+
disabled={loading}
|
|
164
|
+
aria-label={buttonLabel}
|
|
165
|
+
sx={{
|
|
166
|
+
pl: '15px',
|
|
167
|
+
m: 0,
|
|
168
|
+
border: 'none',
|
|
169
|
+
typography: 'h4',
|
|
170
|
+
color: 'var(--black)',
|
|
171
|
+
display: 'flex',
|
|
172
|
+
justifyContent: 'flex-start',
|
|
173
|
+
bgcolor: 'var(--green)',
|
|
174
|
+
height: '71px',
|
|
175
|
+
maxWidth: '400px!important',
|
|
176
|
+
width: '100%',
|
|
177
|
+
borderRadius: '0px',
|
|
178
|
+
opacity: 1,
|
|
179
|
+
minHeight: '60px',
|
|
180
|
+
'&:hover': {
|
|
181
|
+
color: 'var(--green)',
|
|
182
|
+
},
|
|
183
|
+
boxShadow: 'none',
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
{buttonLabel}
|
|
187
|
+
</Button>
|
|
188
|
+
<br />
|
|
189
|
+
{message !== '' && (
|
|
190
|
+
<Typography variant='body1' sx={{ color: 'var(--red)', mt: 2 }}>
|
|
191
|
+
{message}
|
|
192
|
+
</Typography>
|
|
193
|
+
)}
|
|
194
|
+
</Grid>
|
|
195
|
+
|
|
196
|
+
<Grid item xs={12} sm={6} sx={{ zIndex: 1 }}>
|
|
197
|
+
<Typography variant='body1' sx={{ textWrap: 'pretty' }}>
|
|
198
|
+
{description}
|
|
199
|
+
</Typography>
|
|
200
|
+
<br />
|
|
201
|
+
<Typography variant='body2' sx={{ textWrap: 'pretty' }}>
|
|
202
|
+
{subtitle}
|
|
203
|
+
</Typography>
|
|
204
|
+
</Grid>
|
|
205
|
+
|
|
206
|
+
{/* Version footer */}
|
|
207
|
+
<Grid item xs={12} sx={{
|
|
208
|
+
mt: 'auto',
|
|
209
|
+
pt: 8,
|
|
210
|
+
opacity: 0.5,
|
|
211
|
+
zIndex: 1
|
|
212
|
+
}}>
|
|
213
|
+
<Typography variant='body2' sx={{ fontSize: '0.75rem' }}>
|
|
214
|
+
@liiift-studio/sales-portal v{version}
|
|
215
|
+
</Typography>
|
|
216
|
+
</Grid>
|
|
217
|
+
</Grid>
|
|
218
|
+
);
|
|
219
|
+
}
|