Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 63 additions & 37 deletions ui/src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import clsx from 'clsx'
import { JSXElement, Show } from 'solid-js'
import { JSX, JSXElement, Show } from 'solid-js'

type DaisyUIButtonColor =
| 'neutral'
Expand All @@ -15,83 +15,109 @@ type DaisyUIButtonVariant = 'outline' | 'dash' | 'soft' | 'ghost' | 'link'

type DaisyUIButtonSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'

/**
* Renders a button component with an optional loading state.
* If the loading state is passed the button will be disabled and show
* a loader ball while loading.
*/
export function Button(props: {
label: string
icon?: string
type DaisyUIButtonShape = 'square' | 'circle'

interface ButtonBaseProps {
onClick?: (event?: MouseEvent) => void
isLoading?: boolean
disabled?: boolean
type?: 'submit' | 'button' | 'reset'
color?: DaisyUIButtonColor
size?: DaisyUIButtonSize
variant?: DaisyUIButtonVariant
shape?: DaisyUIButtonShape
block?: boolean
class?: string
style?: JSX.CSSProperties
dataCy?: string
}): JSXElement {
ref?: (el: HTMLButtonElement) => void
}

/**
* Renders a button component with an optional loading state.
* If the loading state is passed the button will be disabled and show
* a loader ball while loading.
*/
export function Button(
props: ButtonBaseProps & {
label: JSXElement
icon?: string
trailingIcon?: string
}
): JSXElement {
const isDisabled = () => props.disabled || props.isLoading

return (
<button
ref={props.ref}
class={clsx(
'btn',
props.color ? `btn-${props.color}` : undefined,
props.variant ? `btn-${props.variant}` : undefined,
props.size ? `btn-${props.size}` : undefined,
(props.isLoading || props.disabled) && 'btn-disabled',
props.color && `btn-${props.color}`,
props.variant && `btn-${props.variant}`,
props.size && `btn-${props.size}`,
props.shape && `btn-${props.shape}`,
props.block && 'btn-block',
props.class
)}
type={props.type ?? 'button'}
disabled={isDisabled()}
style={props.style}
onClick={(event) => props.onClick?.(event)}
data-cy={props.dataCy}
>
<Show when={props.icon}>
<i class={props.icon} />
</Show>

{props.label}

<Show when={props.isLoading}>
<Show
when={props.isLoading}
fallback={
<Show when={props.icon}>
<i class={props.icon} />
</Show>
}
>
<span
class={clsx(
'loading loading-ball',
props.size ? `loading-${props.size}` : undefined
props.size && `loading-${props.size}`
)}
/>
</Show>

{props.label}

<Show when={props.trailingIcon && !props.isLoading}>
<i class={props.trailingIcon} />
</Show>
</button>
)
}

export function IconButton(props: {
icon: string
onClick?: () => void
isLoading?: boolean
disabled?: boolean
color?: DaisyUIButtonColor
size?: DaisyUIButtonSize
variant?: DaisyUIButtonVariant
class?: string
dataCy?: string
}): JSXElement {
export function IconButton(
props: ButtonBaseProps & {
icon: string
}
): JSXElement {
const isDisabled = () => props.disabled || props.isLoading

return (
<button
ref={props.ref}
class={clsx(
'btn',
props.variant ? `btn-${props.variant}` : 'btn-ghost',
props.size ? `btn-${props.size}` : 'btn-sm',
props.isLoading && 'btn-disabled',
props.shape && `btn-${props.shape}`,
props.class
)}
onClick={() => props.onClick?.()}
disabled={props.disabled}
type={props.type ?? 'button'}
disabled={isDisabled()}
style={props.style}
onClick={(event) => props.onClick?.(event)}
data-cy={props.dataCy}
>
<Show
when={props.isLoading}
fallback={<i class={clsx(props.icon, `text-${props.color}`)} />}
fallback={
<i class={clsx(props.icon, props.color && `text-${props.color}`)} />
}
>
<span class="loading loading-ball loading-xs" />
</Show>
Expand Down
40 changes: 23 additions & 17 deletions ui/src/components/FrozenWorkspaceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { JSXElement, Show } from 'solid-js'
import { Portal } from 'solid-js/web'
import clsx from 'clsx'

import { Button } from './Button'
import { useLocale } from '../context/LocaleProvider'
import type { FrozenReason } from '../models/Workspace'

Expand Down Expand Up @@ -81,26 +82,31 @@ export function FrozenWorkspaceModal(props: Props): JSXElement {

{/* Actions */}
<div class="flex justify-end gap-3">
<button
class="btn btn-ghost btn-sm h-10 px-6 text-sm font-semibold"
<Button
variant="ghost"
size="sm"
class="h-10 px-6 text-sm font-semibold"
onClick={() => props.onClose()}
>
<Show when={isTrialExpired()} fallback={t('close')}>
{t('later')}
</Show>
</button>
<button
class="btn btn-sm h-10 px-6 text-sm font-semibold border-0 text-white btn-disabled"
label={
<Show when={isTrialExpired()} fallback={t('close')}>
{t('later')}
</Show>
}
/>
<Button
size="sm"
class="h-10 px-6 text-sm font-semibold border-0 text-white"
style={{ background: '#95a5a6' }}
disabled={true}
>
<Show
when={isTrialExpired()}
fallback={t('frozen_subscribe_button')}
>
{t('frozen_upgrade_button')}
</Show>
</button>
label={
<Show
when={isTrialExpired()}
fallback={t('frozen_subscribe_button')}
>
{t('frozen_upgrade_button')}
</Show>
}
/>
</div>
<p class="text-xs text-base-content/50 mt-2 text-right">
{t('upgrade_available_soon')}
Expand Down
20 changes: 11 additions & 9 deletions ui/src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createEffect, JSXElement, For } from 'solid-js'

import { Button } from './Button'
import { useLocale, locales } from '../context/LocaleProvider'

export function LanguageSelector(): JSXElement {
Expand All @@ -23,19 +24,20 @@ export function LanguageSelector(): JSXElement {
<For each={locales}>
{(language) => (
<li>
<button
class="btn btn-ghost"
data-cy={`language-${language}`}
<Button
variant="ghost"
dataCy={`language-${language}`}
onClick={() => {
setLocale(language)
detailsRef?.removeAttribute('open')
}}
>
<div class="flex gap-2 mr-2">
<CountryFlag countryCode={language} />
<span>{language}</span>
</div>
</button>
label={
<div class="flex gap-2 mr-2">
<CountryFlag countryCode={language} />
<span>{language}</span>
</div>
}
/>
</li>
)}
</For>
Expand Down
14 changes: 9 additions & 5 deletions ui/src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import clsx from 'clsx'
import { createSignal, JSXElement } from 'solid-js'
import { Portal } from 'solid-js/web'

import { Button } from './Button'

/**
* Creates a modal state that can be used to open and close modals
* the keys passed in are used as identifiers for identifying differnent modals.
Expand Down Expand Up @@ -43,12 +45,14 @@ export function Modal(props: {
class={clsx('modal z-1000', isOpen() ? 'modal-open' : 'modal-close')}
>
<div class="modal-box w-90vw max-w-sm sm:max-w-md">
<button
class="btn btn-sm btn-circle btn-ghost absolute right-4 top-4"
<Button
variant="ghost"
shape="circle"
size="sm"
class="absolute right-4 top-4"
onClick={() => props.onClose()}
>
</button>
label="✕"
/>
<h3 class="font-bold text-lg">{props.title}</h3>
{props.children}
</div>
Expand Down
29 changes: 11 additions & 18 deletions ui/src/components/ProfileMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createSignal, JSXElement, Show } from 'solid-js'
import { A, useNavigate } from '@solidjs/router'
import { clsx } from 'clsx'

import { useUser } from '../context/UserProvider'
import { useLocale } from '../context/LocaleProvider'
import { Button } from './Button'
import { Toast } from './Toast'

import { logout } from '../api'
Expand Down Expand Up @@ -65,24 +65,17 @@ export function ProfileMenu(): JSXElement {
</Show>

<li>
<button
class={clsx(
'btn',
'btn-ghost',
'justify-start',
loggingOut() && 'btn-disabled'
)}
<Button
variant="ghost"
class="justify-start"
icon={
loggingOut() ? undefined : 'fa-solid fa-arrow-right-from-bracket'
}
isLoading={loggingOut()}
onClick={onLogout}
data-cy="logout"
>
<Show
when={loggingOut()}
fallback={<i class="fa-solid fa-arrow-right-from-bracket" />}
>
<span class="loading loading-ball text-neutral loading-sm" />
</Show>
{t('logout')}
</button>
dataCy="logout"
label={t('logout')}
/>
</li>
</ul>

Expand Down
Loading
Loading