announcements

This commit is contained in:
pluja
2025-05-19 16:57:10 +00:00
parent 205b6e8ea0
commit 636057f8e0
26 changed files with 1966 additions and 659 deletions

View File

@@ -0,0 +1,82 @@
---
import { Icon } from 'astro-icon/components'
import { Markdown } from 'astro-remote'
import type { AnnouncementType } from '@prisma/client'
export type Announcement = {
id: number
title: string
content: string
type: AnnouncementType
startDate: Date
endDate: Date | null
isActive: boolean
}
export type Props = {
announcements: Announcement[]
}
const { announcements } = Astro.props
// Get icon and class based on announcement type
const getTypeInfo = (type: AnnouncementType) => {
switch (type) {
case 'INFO':
return {
icon: 'ri:information-line',
containerClass: 'bg-blue-900/40 border-blue-500/30',
titleClass: 'text-blue-400',
contentClass: 'text-blue-300',
}
case 'WARNING':
return {
icon: 'ri:alert-line',
containerClass: 'bg-yellow-900/40 border-yellow-500/30',
titleClass: 'text-yellow-400',
contentClass: 'text-yellow-300',
}
case 'ALERT':
return {
icon: 'ri:error-warning-line',
containerClass: 'bg-red-900/40 border-red-500/30',
titleClass: 'text-red-400',
contentClass: 'text-red-300',
}
default:
return {
icon: 'ri:information-line',
containerClass: 'bg-blue-900/40 border-blue-500/30',
titleClass: 'text-blue-400',
contentClass: 'text-blue-300',
}
}
}
---
{
announcements.length > 0 && (
<div class="mb-4 flex flex-col items-center space-y-1">
{announcements.map((announcement) => {
const typeInfo = getTypeInfo(announcement.type)
return (
<div
class={`flex flex-row items-center rounded border ${typeInfo.containerClass} mx-auto w-auto max-w-full px-3 py-2`}
>
<Icon name={typeInfo.icon} class={`size-4 flex-shrink-0 ${typeInfo.titleClass} mr-2`} />
<div class="flex min-w-0 flex-col">
<span class={`text-sm leading-tight font-bold ${typeInfo.titleClass} truncate`}>
{announcement.title}
</span>
<span class={`text-xs ${typeInfo.contentClass} truncate leading-snug [&_a]:underline`}>
<Markdown content={announcement.content} />
</span>
</div>
</div>
)
})}
</div>
)
}

View File

@@ -9,29 +9,33 @@ import InputWrapper from './InputWrapper.astro'
import type { MarkdownString } from '../lib/markdown'
import type { ComponentProps } from 'astro/types'
type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId'> & {
type Props<Multiple extends boolean = false> = Omit<
ComponentProps<typeof InputWrapper>,
'children' | 'inputId'
> & {
options: {
label: string
value: string
icon?: string
iconClass?: string
description?: MarkdownString
disabled?: boolean
}[]
disabled?: boolean
selectedValue?: string
selectedValue?: Multiple extends true ? string[] : string
cardSize?: 'lg' | 'md' | 'sm'
iconSize?: 'md' | 'sm'
multiple?: boolean
multiple?: Multiple
}
const {
options,
disabled,
selectedValue,
selectedValue = undefined as string[] | string | undefined,
cardSize = 'sm',
iconSize = 'sm',
class: className,
multiple,
multiple = false as boolean,
...wrapperProps
} = Astro.props
@@ -40,6 +44,7 @@ const inputId = Astro.locals.makeId(`input-${wrapperProps.name}`)
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
---
{/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */}
<InputWrapper inputId={inputId} class={cn('@container', className)} {...wrapperProps}>
<div
class={cn(
@@ -62,7 +67,7 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
'has-checked:border-green-700 has-checked:bg-green-700/20 has-checked:ring-1 has-checked:ring-green-700',
multiple &&
'has-focus-visible:border-day-300 has-focus-visible:ring-2 has-focus-visible:ring-green-700 has-focus-visible:ring-offset-1',
disabled && 'cursor-not-allowed opacity-50'
'has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50'
)}
>
<input
@@ -70,9 +75,13 @@ const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
type={multiple ? 'checkbox' : 'radio'}
name={wrapperProps.name}
value={option.value}
checked={selectedValue === option.value}
checked={
Array.isArray(selectedValue)
? selectedValue.includes(option.value)
: selectedValue === option.value
}
class="peer sr-only"
disabled={disabled}
disabled={disabled || option.disabled}
/>
<div class="flex items-center gap-1.5">
{option.icon && (

View File

@@ -0,0 +1,48 @@
---
import { omit } from 'lodash-es'
import { cn } from '../lib/cn'
import { baseInputClassNames } from '../lib/formInputs'
import InputWrapper from './InputWrapper.astro'
import type { ComponentProps, HTMLAttributes } from 'astro/types'
type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId' | 'required'> & {
options: {
label: string
value: string
disabled?: boolean
}[]
selectProps?: Omit<HTMLAttributes<'select'>, 'name'>
}
const { options, selectProps, ...wrapperProps } = Astro.props
const inputId = selectProps?.id ?? Astro.locals.makeId(`input-${wrapperProps.name}`)
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
---
<InputWrapper inputId={inputId} required={selectProps?.required} {...wrapperProps}>
<select
transition:persist
{...omit(selectProps, ['class', 'id', 'name'])}
id={inputId}
class={cn(
baseInputClassNames.input,
'appearance-none',
selectProps?.class,
hasError && baseInputClassNames.error,
!!selectProps?.disabled && baseInputClassNames.disabled
)}
name={wrapperProps.name}
>
{
options.map((option) => (
<option value={option.value} disabled={option.disabled}>
{option.label}
</option>
))
}
</select>
</InputWrapper>

View File

@@ -1,44 +1,36 @@
---
import { omit } from 'lodash-es'
import { cn } from '../lib/cn'
import { baseInputClassNames } from '../lib/formInputs'
import InputWrapper from './InputWrapper.astro'
import type { ComponentProps } from 'astro/types'
import type { ComponentProps, HTMLAttributes } from 'astro/types'
type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId'> & {
type Props = Omit<ComponentProps<typeof InputWrapper>, 'children' | 'inputId' | 'required'> & {
inputProps?: Omit<HTMLAttributes<'textarea'>, 'name'>
value?: string
placeholder?: string
disabled?: boolean
autofocus?: boolean
rows?: number
maxlength?: number
}
const { value, placeholder, maxlength, disabled, autofocus, rows = 3, ...wrapperProps } = Astro.props
const { inputProps, value, ...wrapperProps } = Astro.props
const inputId = Astro.locals.makeId(`input-${wrapperProps.name}`)
const inputId = inputProps?.id ?? Astro.locals.makeId(`input-${wrapperProps.name}`)
const hasError = !!wrapperProps.error && wrapperProps.error.length > 0
---
{/* eslint-disable astro/jsx-a11y/no-autofocus */}
<InputWrapper inputId={inputId} {...wrapperProps}>
<InputWrapper inputId={inputId} required={inputProps?.required} {...wrapperProps}>
<textarea
transition:persist
{...omit(inputProps, ['class', 'id', 'name'])}
id={inputId}
class={cn(
baseInputClassNames.input,
baseInputClassNames.textarea,
inputProps?.class,
hasError && baseInputClassNames.error,
disabled && baseInputClassNames.disabled
!!inputProps?.disabled && baseInputClassNames.disabled
)}
placeholder={placeholder}
required={wrapperProps.required}
disabled={disabled}
name={wrapperProps.name}
autofocus={autofocus}
maxlength={maxlength}
rows={rows}>{value}</textarea
name={wrapperProps.name}>{value}</textarea
>
</InputWrapper>

View File

@@ -66,7 +66,7 @@ const hasError = !!error && error.length > 0
{
!!description && (
<div class="prose prose-sm prose-invert prose-a:text-current prose-a:font-normal hover:prose-a:text-day-300 prose-a:transition-colors text-day-400 text-xs text-pretty">
<div class="prose prose-sm prose-invert prose-a:text-current prose-a:font-normal hover:prose-a:text-day-300 prose-a:transition-colors text-day-400 max-w-none text-xs text-pretty">
<Markdown content={description} />
</div>
)