Skip to content

Commit f4a69ea

Browse files
committed
feat: alert and button styles redesign
1 parent 12a6f22 commit f4a69ea

File tree

6 files changed

+344
-75
lines changed

6 files changed

+344
-75
lines changed

admin/app/services/map_service.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ export class MapService {
125125
throw new Error('Base styles file not found in storage/maps')
126126
}
127127

128-
const localUrl = env.get('URL')
129128
const rawStyles = JSON.parse(baseStyle.toString()) as BaseStylesFile
130129

131130
const regions = (await this.listRegions()).files
132131
const sources = this.generateSourcesArray(regions)
133132

134-
const baseUrl = urlJoin(localUrl, this.mapStoragePath, this.basemapsAssetsDir)
133+
const localUrl = env.get('URL')
134+
const withProtocol = localUrl.startsWith('http') ? localUrl : `http://${localUrl}`
135+
const baseUrlPath = urlJoin(this.mapStoragePath, this.basemapsAssetsDir)
136+
const baseUrl = new URL(baseUrlPath, withProtocol).toString()
135137

136138
const styles = await this.generateStylesFile(
137139
rawStyles,
@@ -173,10 +175,15 @@ export class MapService {
173175
if (region.type === 'file' && region.name.endsWith('.pmtiles')) {
174176
const regionName = region.name.replace('.pmtiles', '')
175177
const source: BaseStylesFile['sources'] = {}
178+
const sourceUrl = new URL(
179+
urlJoin(this.mapStoragePath, 'pmtiles', region.name),
180+
localUrl.startsWith('http') ? localUrl : `http://${localUrl}`
181+
).toString()
182+
176183
source[regionName] = {
177184
type: 'vector',
178185
attribution: PMTILES_ATTRIBUTION,
179-
url: `pmtiles://http://${urlJoin(localUrl, this.mapStoragePath, 'pmtiles', region.name)}`,
186+
url: `pmtiles://${sourceUrl}`,
180187
}
181188
sources.push(source)
182189
}

admin/inertia/components/Alert.tsx

Lines changed: 181 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,200 @@
1-
import { ExclamationTriangleIcon, XCircleIcon } from '@heroicons/react/24/solid'
2-
import { IconCircleCheck } from '@tabler/icons-react'
1+
import * as Icons from '@heroicons/react/24/solid'
32
import classNames from '~/lib/classNames'
43

54
export type AlertProps = React.HTMLAttributes<HTMLDivElement> & {
65
title: string
76
message?: string
8-
type: 'warning' | 'error' | 'success'
7+
type: 'warning' | 'error' | 'success' | 'info'
98
children?: React.ReactNode
9+
dismissible?: boolean
10+
onDismiss?: () => void
11+
icon?: keyof typeof Icons
12+
variant?: 'standard' | 'bordered' | 'solid'
1013
}
1114

12-
export default function Alert({ title, message, type, children, ...props }: AlertProps) {
13-
const getIcon = () => {
14-
const Icon =
15-
type === 'warning'
16-
? ExclamationTriangleIcon
17-
: type === 'error'
18-
? XCircleIcon
19-
: IconCircleCheck
20-
const color =
21-
type === 'warning' ? 'text-yellow-400' : type === 'error' ? 'text-red-400' : 'text-green-400'
22-
23-
return <Icon aria-hidden="true" className={`size-5 ${color}`} />
15+
export default function Alert({
16+
title,
17+
message,
18+
type,
19+
children,
20+
dismissible = false,
21+
onDismiss,
22+
icon,
23+
variant = 'standard',
24+
...props
25+
}: AlertProps) {
26+
const getDefaultIcon = (): keyof typeof Icons => {
27+
switch (type) {
28+
case 'warning':
29+
return 'ExclamationTriangleIcon'
30+
case 'error':
31+
return 'XCircleIcon'
32+
case 'success':
33+
return 'CheckCircleIcon'
34+
case 'info':
35+
return 'InformationCircleIcon'
36+
default:
37+
return 'InformationCircleIcon'
38+
}
2439
}
2540

26-
const getBackground = () => {
27-
return type === 'warning' ? 'bg-yellow-100' : type === 'error' ? 'bg-red-50' : 'bg-green-50'
41+
const IconComponent = () => {
42+
const iconName = icon || getDefaultIcon()
43+
const Icon = Icons[iconName]
44+
if (!Icon) return null
45+
46+
return <Icon aria-hidden="true" className={classNames('size-5 shrink-0', getIconColor())} />
2847
}
2948

30-
const getTextColor = () => {
31-
return type === 'warning'
32-
? 'text-yellow-800'
33-
: type === 'error'
34-
? 'text-red-800'
35-
: 'text-green-800'
49+
const getIconColor = () => {
50+
if (variant === 'solid') return 'text-desert-white'
51+
switch (type) {
52+
case 'warning':
53+
return 'text-desert-orange'
54+
case 'error':
55+
return 'text-desert-red'
56+
case 'success':
57+
return 'text-desert-olive'
58+
case 'info':
59+
return 'text-desert-stone'
60+
default:
61+
return 'text-desert-stone'
62+
}
63+
}
64+
65+
const getVariantStyles = () => {
66+
const baseStyles = 'rounded-md transition-all duration-200'
67+
const variantStyles: string[] = []
68+
69+
switch (variant) {
70+
case 'bordered':
71+
variantStyles.push(
72+
type === 'warning'
73+
? 'border-desert-orange'
74+
: type === 'error'
75+
? 'border-desert-red'
76+
: type === 'success'
77+
? 'border-desert-olive'
78+
: type === 'info'
79+
? 'border-desert-stone'
80+
: ''
81+
)
82+
return classNames(baseStyles, 'border-2 bg-desert-white', ...variantStyles)
83+
case 'solid':
84+
variantStyles.push(
85+
type === 'warning'
86+
? 'bg-desert-orange text-desert-white border-desert-orange-dark'
87+
: type === 'error'
88+
? 'bg-desert-red text-desert-white border-desert-red-dark'
89+
: type === 'success'
90+
? 'bg-desert-olive text-desert-white border-desert-olive-dark'
91+
: type === 'info'
92+
? 'bg-desert-stone text-desert-white border-desert-stone-dark'
93+
: ''
94+
)
95+
return classNames(baseStyles, 'shadow-sm', ...variantStyles)
96+
default:
97+
variantStyles.push(
98+
type === 'warning'
99+
? 'bg-desert-orange-lighter bg-opacity-20 border-desert-orange-light'
100+
: type === 'error'
101+
? 'bg-desert-red-lighter bg-opacity-20 border-desert-red-light'
102+
: type === 'success'
103+
? 'bg-desert-olive-lighter bg-opacity-20 border-desert-olive-light'
104+
: type === 'info'
105+
? 'bg-desert-stone-lighter bg-opacity-20 border-desert-stone-light'
106+
: ''
107+
)
108+
return classNames(baseStyles, 'border shadow-sm', ...variantStyles)
109+
}
110+
}
111+
112+
const getTitleColor = () => {
113+
if (variant === 'solid') return 'text-desert-white'
114+
115+
switch (type) {
116+
case 'warning':
117+
return 'text-desert-orange-dark'
118+
case 'error':
119+
return 'text-desert-red-dark'
120+
case 'success':
121+
return 'text-desert-olive-dark'
122+
case 'info':
123+
return 'text-desert-stone-dark'
124+
default:
125+
return 'text-desert-stone-dark'
126+
}
127+
}
128+
129+
const getMessageColor = () => {
130+
if (variant === 'solid') return 'text-desert-white text-opacity-90'
131+
132+
switch (type) {
133+
case 'warning':
134+
return 'text-desert-orange-dark text-opacity-80'
135+
case 'error':
136+
return 'text-desert-red-dark text-opacity-80'
137+
case 'success':
138+
return 'text-desert-olive-dark text-opacity-80'
139+
case 'info':
140+
return 'text-desert-stone-dark text-opacity-80'
141+
default:
142+
return 'text-desert-stone-dark text-opacity-80'
143+
}
144+
}
145+
146+
const getCloseButtonStyles = () => {
147+
if (variant === 'solid') {
148+
return 'text-desert-white hover:text-desert-white hover:bg-black hover:bg-opacity-20'
149+
}
150+
151+
switch (type) {
152+
case 'warning':
153+
return 'text-desert-orange hover:text-desert-orange-dark hover:bg-desert-orange-lighter hover:bg-opacity-30'
154+
case 'error':
155+
return 'text-desert-red hover:text-desert-red-dark hover:bg-desert-red-lighter hover:bg-opacity-30'
156+
case 'success':
157+
return 'text-desert-olive hover:text-desert-olive-dark hover:bg-desert-olive-lighter hover:bg-opacity-30'
158+
case 'info':
159+
return 'text-desert-stone hover:text-desert-stone-dark hover:bg-desert-stone-lighter hover:bg-opacity-30'
160+
default:
161+
return 'text-desert-stone hover:text-desert-stone-dark hover:bg-desert-stone-lighter hover:bg-opacity-30'
162+
}
36163
}
37164

38165
return (
39-
<div
40-
{...props}
41-
className={classNames(
42-
getBackground(),
43-
props.className,
44-
'border border-gray-200 rounded-md p-3 shadow-xs'
45-
)}
46-
>
47-
<div className="flex flex-row justify-between items-center">
48-
<div className="flex flex-row">
49-
<div className="shrink-0">{getIcon()}</div>
50-
<div className="ml-3">
51-
<h3 className={`text-sm font-medium ${getTextColor()}`}>{title}</h3>
52-
{message && (
53-
<div className={`mt-2 text-sm ${getTextColor()}`}>
54-
<p>{message}</p>
55-
</div>
56-
)}
57-
</div>
166+
<div {...props} className={classNames(getVariantStyles(), 'p-4', props.className)} role="alert">
167+
<div className="flex gap-3">
168+
<IconComponent />
169+
170+
<div className="flex-1 min-w-0">
171+
<h3 className={classNames('text-sm font-semibold', getTitleColor())}>{title}</h3>
172+
{message && (
173+
<div className={classNames('mt-1 text-sm', getMessageColor())}>
174+
<p>{message}</p>
175+
</div>
176+
)}
177+
{children && <div className="mt-3">{children}</div>}
58178
</div>
59-
{children}
179+
180+
{dismissible && (
181+
<button
182+
type="button"
183+
onClick={onDismiss}
184+
className={classNames(
185+
'shrink-0 rounded-md p-1.5 transition-colors duration-150',
186+
getCloseButtonStyles(),
187+
'focus:outline-none focus:ring-2 focus:ring-offset-2',
188+
type === 'warning' ? 'focus:ring-desert-orange' : '',
189+
type === 'error' ? 'focus:ring-desert-red' : '',
190+
type === 'success' ? 'focus:ring-desert-olive' : '',
191+
type === 'info' ? 'focus:ring-desert-stone' : ''
192+
)}
193+
aria-label="Dismiss alert"
194+
>
195+
<Icons.XMarkIcon className="size-5" />
196+
</button>
197+
)}
60198
</div>
61199
</div>
62200
)

0 commit comments

Comments
 (0)