Contact Page

This commit is contained in:
Ahmed Al-Taiar
2024-08-21 01:24:24 -04:00
parent c7d87e36f2
commit 593567a197
25 changed files with 848 additions and 42 deletions

View File

@ -0,0 +1,7 @@
-- CreateTable
CREATE TABLE "Portrait" (
"id" SERIAL NOT NULL,
"fileId" TEXT NOT NULL,
CONSTRAINT "Portrait_pkey" PRIMARY KEY ("id")
);

View File

@ -43,3 +43,8 @@ model Social {
type Handle
username String
}
model Portrait {
id Int @id @default(autoincrement())
fileId String
}

View File

@ -0,0 +1,19 @@
export const schema = gql`
type Portrait {
id: Int!
fileId: String!
}
type Query {
portrait: Portrait @skipAuth
}
input CreatePortraitInput {
fileId: String!
}
type Mutation {
createPortrait(input: CreatePortraitInput!): Portrait! @requireAuth
deletePortrait: Portrait! @requireAuth
}
`

View File

@ -20,8 +20,8 @@ export const schema = gql`
}
type Query {
socials: [Social!]! @requireAuth
social(id: Int!): Social @requireAuth
socials: [Social!]! @skipAuth
social(id: Int!): Social @skipAuth
}
input CreateSocialInput {

View File

@ -39,7 +39,10 @@ export const handleTusUpload = (
res.raw.statusCode = 405
res.raw.end('Method not allowed')
}
} else tusHandler.handle(req.raw, res.raw)
} else {
setCorsHeaders(res)
tusHandler.handle(req.raw, res.raw)
}
}
const handleAuthenticatedRequest = async (

View File

@ -0,0 +1,37 @@
import type { QueryResolvers, MutationResolvers } from 'types/graphql'
import { ValidationError } from '@redwoodjs/graphql-server'
import { db } from 'src/lib/db'
export const portrait: QueryResolvers['portrait'] = async () => {
const portrait = await db.portrait.findFirst()
if (portrait) return portrait
else
return {
id: -1,
fileId: '/no_portrait.webp',
}
}
export const createPortrait: MutationResolvers['createPortrait'] = async ({
input,
}) => {
if (await db.portrait.findFirst())
throw new ValidationError('Portrait already exists')
else
return db.portrait.create({
data: input,
})
}
export const deletePortrait: MutationResolvers['deletePortrait'] = async () => {
const portrait = await db.portrait.findFirst()
if (!portrait) throw new ValidationError('Portrait does not exist')
else
return db.portrait.delete({
where: { id: portrait.id },
})
}

View File

@ -31,6 +31,13 @@ export const theme = {
extend: {
width: {
46: '11.5rem',
54: '13.5rem',
},
maxWidth: {
68: '17rem',
},
aspectRatio: {
portrait: '4 / 5',
},
fontFamily: {
syne: ['Syne', 'sans-serif'],

View File

@ -23,7 +23,6 @@
"@uppy/dashboard": "^4.0.2",
"@uppy/drag-drop": "^4.0.1",
"@uppy/file-input": "^4.0.0",
"@uppy/image-editor": "^3.0.0",
"@uppy/progress-bar": "^4.0.0",
"@uppy/react": "^4.0.1",
"@uppy/tus": "^4.0.0",

BIN
web/public/no_portrait.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -4,6 +4,7 @@ import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
import './scaffold.css'
import { AuthProvider, useAuth } from './auth'
import './index.css'

View File

@ -16,6 +16,10 @@ const Routes = () => {
<Route path="/socials/{id:Int}" page={SocialSocialPage} name="social" />
<Route path="/socials" page={SocialSocialsPage} name="socials" />
</Set>
<Set wrap={ScaffoldLayout} title="Portrait" titleTo="portrait">
<Route path="/portrait" page={PortraitPortraitPage} name="portrait" />
</Set>
</PrivateSet>
<Set wrap={AccountbarLayout} title="Login">
@ -32,6 +36,7 @@ const Routes = () => {
<Set wrap={NavbarLayout}>
<Route path="/" page={HomePage} name="home" />
<Route path="/contact" page={ContactPage} name="contact" />
</Set>
<Route notfound page={NotFoundPage} />

View File

@ -0,0 +1,80 @@
import { useState, useRef, useEffect } from 'react'
import SocialLinksCell from 'src/components/Social/SocialLinksCell'
interface ContactCardProps {
portraitUrl: string
}
const ContactCard = ({ portraitUrl }: ContactCardProps) => {
const [width, setWidth] = useState()
const [height, setHeight] = useState()
const observedDiv = useRef(null)
useEffect(() => {
if (!observedDiv.current) return
const resizeObserver = new ResizeObserver(() => {
if (observedDiv.current.offsetWidth !== width)
setWidth(observedDiv.current.offsetWidth)
if (observedDiv.current.offsetHeight !== height)
setHeight(observedDiv.current.offsetHeight)
})
resizeObserver.observe(observedDiv.current)
return function cleanup() {
resizeObserver.disconnect()
}
}, [width, height])
return (
<>
<style
dangerouslySetInnerHTML={{
/* Tailwind arbitrary classes (e.g h-[${height}px]) doesn't work? */
__html: `
.contact-me-image {
width: ${width}px;
height: auto;
@media (min-width: 768px) {
width: auto;
min-height: 18.25rem;
height: ${height}px;
}
}
`,
}}
/>
<div className="flex min-h-[calc(100vh-6rem)] items-center justify-center">
<div className="card bg-base-100 shadow-xl md:card-side">
<figure>
<img
className="contact-me-image aspect-portrait object-cover"
src={portraitUrl}
alt={`${process.env.NAME}`}
/>
</figure>
<div
className="card-body mx-auto h-fit w-fit md:mx-0"
ref={observedDiv}
>
<h2 className="card-title justify-center text-3xl md:justify-start">
Contact Me
</h2>
<p className="p-2"></p>
<div className="card-actions">
<SocialLinksCell />
</div>
</div>
</div>
</div>
</>
)
}
export default ContactCard

View File

@ -0,0 +1,37 @@
import type { FindPortrait, FindPortraitVariables } from 'types/graphql'
import type {
TypedDocumentNode,
CellFailureProps,
CellSuccessProps,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import ContactCard from 'src/components/ContactCard/ContactCard/ContactCard'
export const QUERY: TypedDocumentNode<
FindPortrait,
FindPortraitVariables
> = gql`
query FindPortrait {
portrait: portrait {
fileId
}
}
`
export const Loading = () => <CellLoading />
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindPortrait>) => (
<CellFailure error={error} />
)
export const Success = ({
portrait,
}: CellSuccessProps<FindPortrait, FindPortraitVariables>) => (
<ContactCard portraitUrl={portrait.fileId} />
)

View File

@ -0,0 +1,37 @@
import type { FindPortrait, FindPortraitVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import PortraitForm from 'src/components/Portrait/PortraitForm/PortraitForm'
export const QUERY: TypedDocumentNode<
FindPortrait,
FindPortraitVariables
> = gql`
query FindPortrait {
portrait: portrait {
id
fileId
}
}
`
export const Loading = () => <CellLoading />
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindPortraitVariables>) => (
<CellFailure error={error} />
)
export const Success = ({
portrait,
}: CellSuccessProps<FindPortrait, FindPortraitVariables>) =>
portrait.id === -1 ? <PortraitForm /> : <PortraitForm portrait={portrait} />

View File

@ -0,0 +1,213 @@
import { useRef, useState } from 'react'
import { Meta, UploadResult } from '@uppy/core'
import type {
CreatePortraitMutation,
CreatePortraitMutationVariables,
DeletePortraitMutation,
DeletePortraitMutationVariables,
EditPortrait,
FindPortrait,
FindPortraitVariables,
} from 'types/graphql'
import { TypedDocumentNode, useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/dist/toast'
import Uploader from 'src/components/Uploader/Uploader'
interface PortraitFormProps {
portrait?: EditPortrait['portrait']
}
export const QUERY: TypedDocumentNode<
FindPortrait,
FindPortraitVariables
> = gql`
query FindPortrait {
portrait {
id
fileId
}
}
`
const DELETE_PORTRAIT_MUTATION: TypedDocumentNode<
DeletePortraitMutation,
DeletePortraitMutationVariables
> = gql`
mutation DeletePortraitMutation {
deletePortrait {
id
fileId
}
}
`
const CREATE_PORTRAIT_MUTATION: TypedDocumentNode<
CreatePortraitMutation,
CreatePortraitMutationVariables
> = gql`
mutation CreatePortraitMutation($input: CreatePortraitInput!) {
createPortrait(input: $input) {
id
fileId
}
}
`
const PortraitForm = (props: PortraitFormProps) => {
const [fileId, _setFileId] = useState<string>(props.portrait?.fileId)
const fileIdRef = useRef(fileId)
const setFileId = (fileId: string) => {
_setFileId(fileId)
fileIdRef.current = fileId
}
const unloadAbortController = new AbortController()
const [deletePortrait, { loading: deleteLoading }] = useMutation(
DELETE_PORTRAIT_MUTATION,
{
onCompleted: () => {
toast.success('Portrait deleted')
},
onError: (error) => {
toast.error(error.message)
},
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
}
)
const [createPortrait, { loading: createLoading }] = useMutation(
CREATE_PORTRAIT_MUTATION,
{
onCompleted: () => toast.success('Portrait saved'),
onError: (error) => toast.error(error.message),
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
}
)
const handleBeforeUnload = (_e: BeforeUnloadEvent) => {
deleteFile(fileIdRef.current)
if (navigator.userAgent.match(/firefox|fxios/i)) {
// Dear Mozilla, please implement keepalive on Firefox
// Sincerely, everybody
const time = Date.now()
while (Date.now() - time < 500) {
/* empty */
}
}
}
const onUploadComplete = (
result: UploadResult<Meta, Record<string, never>>
) => {
setFileId(result.successful[0]?.uploadURL)
window.addEventListener('beforeunload', handleBeforeUnload, {
once: true,
signal: unloadAbortController.signal,
})
}
if (props.portrait?.fileId)
return (
<div className="mx-auto w-fit space-y-2">
<img
className="aspect-portrait max-w-2xl rounded-xl object-cover"
src={props.portrait?.fileId}
alt={`${process.env.NAME} Portrait`}
/>
<div className="flex justify-center">
<button
type="button"
title="Delete portrait"
className="btn btn-error btn-sm uppercase"
onClick={() => {
if (confirm('Are you sure?')) {
deleteFile(props.portrait?.fileId)
deletePortrait()
setFileId(null)
}
}}
disabled={deleteLoading}
>
Delete
</button>
</div>
</div>
)
else
return (
<div className="mx-auto w-fit space-y-2">
{!fileId ? (
<>
<Uploader
onComplete={onUploadComplete}
width="22rem"
height="11.5rem"
className="flex justify-center"
/>
<p className="text-center">
High quality, 4:5 aspect ratio image recommended
</p>
</>
) : (
<img
className="aspect-portrait max-w-2xl rounded-xl object-cover"
src={fileId}
alt={`${process.env.NAME} Portrait`}
/>
)}
{fileId && (
<div className="flex justify-center space-x-2">
<button
className={`btn btn-sm ${!fileId && 'btn-disabled'} uppercase`}
disabled={!fileId || deleteLoading}
onClick={() => {
deleteFile(fileId)
setFileId(null)
unloadAbortController.abort()
}}
>
Change
</button>
<button
className={`btn btn-primary btn-sm ${
!fileId && 'btn-disabled'
} uppercase`}
disabled={!fileId || createLoading}
onClick={() => {
createPortrait({
variables: {
input: {
fileId,
},
},
})
unloadAbortController.abort()
}}
>
Submit
</button>
</div>
)}
</div>
)
}
const deleteFile = async (fileId: string) => {
await fetch(fileId, {
method: 'DELETE',
headers: {
'Tus-Resumable': '1.0.0',
},
keepalive: true,
})
}
export default PortraitForm

View File

@ -45,12 +45,16 @@ const NewSocial = () => {
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<table className="table w-80">
<thead className="bg-base-200 font-syne">
<th className="w-0">New Social</th>
<tr>
<th>New Social</th>
</tr>
</thead>
<tbody>
<th>
<SocialForm onSave={onSave} error={error} loading={loading} />
</th>
<tr>
<th>
<SocialForm onSave={onSave} error={error} loading={loading} />
</th>
</tr>
</tbody>
</table>
</div>

View File

@ -0,0 +1,26 @@
import { FindSocials } from 'types/graphql'
import { baseUrls, getLogoComponent } from 'src/lib/handle'
const SocialLinks = ({ socials }: FindSocials) => {
return (
<div className={`grid max-w-68 grid-cols-[repeat(auto-fit,3rem)] gap-2`}>
{[...socials]
.sort((a, b) => (a.type > b.type ? 1 : -1))
.map((social, i) => (
<div key={i} className="tooltip" data-tip={social.name}>
<a
className="btn btn-square"
href={`${baseUrls[social.type]}${social.username}`}
target="_blank"
rel="noreferrer"
>
{getLogoComponent(social.type)}
</a>
</div>
))}
</div>
)
}
export default SocialLinks

View File

@ -0,0 +1,35 @@
import type { FindSocials, FindSocialsVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import SocialLinks from 'src/components/Social/SocialLinks/SocialLinks'
export const QUERY: TypedDocumentNode<FindSocials, FindSocialsVariables> = gql`
query SocialsQuery {
socials {
id
name
type
username
}
}
`
export const Loading = () => <CellLoading />
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindSocials>) => (
<CellFailure error={error} />
)
export const Success = ({ socials }: CellSuccessProps<FindSocials>) => (
<SocialLinks socials={socials} />
)

View File

@ -3,7 +3,6 @@ import { useState } from 'react'
import Compressor from '@uppy/compressor'
import Uppy from '@uppy/core'
import type { UploadResult, Meta } from '@uppy/core'
import ImageEditor from '@uppy/image-editor'
import { Dashboard } from '@uppy/react'
import Tus from '@uppy/tus'
@ -11,17 +10,30 @@ import { isProduction } from '@redwoodjs/api/dist/logger'
import '@uppy/core/dist/style.min.css'
import '@uppy/dashboard/dist/style.min.css'
import '@uppy/image-editor/dist/style.min.css'
interface Props {
onComplete?(result: UploadResult<Meta, Record<string, never>>): void
width?: string | number
height?: string | number
className?: string
maxFiles?: number
disabled?: boolean
hidden?: boolean
}
const apiDomain = isProduction
? process.env.API_ADDRESS_PROD
: process.env.API_ADDRESS_DEV
const Uploader = ({ onComplete }: Props) => {
const Uploader = ({
onComplete,
width,
height,
className,
disabled = false,
hidden = false,
maxFiles = 1,
}: Props) => {
const [uppy] = useState(() => {
const instance = new Uppy({
restrictions: {
@ -31,7 +43,7 @@ const Uploader = ({ onComplete }: Props) => {
'image/jpg',
'image/jpeg',
],
maxNumberOfFiles: 10,
maxNumberOfFiles: maxFiles,
maxFileSize: 25 * 1024 * 1024,
},
onBeforeUpload: (files) => {
@ -54,12 +66,21 @@ const Uploader = ({ onComplete }: Props) => {
.use(Compressor, {
mimeType: 'image/webp',
})
.use(ImageEditor)
return instance.on('complete', onComplete)
})
return <Dashboard uppy={uppy} proudlyDisplayPoweredByUppy={false} />
return (
<Dashboard
uppy={uppy}
proudlyDisplayPoweredByUppy={false}
width={width}
height={height}
disabled={disabled}
hidden={hidden}
className={className}
/>
)
}
export default Uploader

View File

@ -19,11 +19,10 @@ type NavbarLayoutProps = {
const NavbarLayout = ({ children }: NavbarLayoutProps) => {
const { isAuthenticated, logOut } = useAuth()
// TODO: populate with buttons to other page
const navbarRoutes: NavbarRoute[] = [
{
name: 'Test',
path: routes.home(),
name: 'Contact',
path: routes.contact(),
},
]
@ -32,6 +31,10 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
name: 'Socials',
path: routes.socials(),
},
{
name: 'Portrait',
path: routes.portrait(),
},
]
const navbarButtons = () =>

View File

@ -0,0 +1,42 @@
import { useEffect, useRef, useState } from 'react'
import { Metadata } from '@redwoodjs/web'
import ContactCardCell from 'src/components/ContactCard/ContactCardCell'
const ContactPage = () => {
const [width, setWidth] = useState()
const [height, setHeight] = useState()
const observedDiv = useRef(null)
useEffect(() => {
if (!observedDiv.current) return
const resizeObserver = new ResizeObserver(() => {
if (observedDiv.current.offsetWidth !== width)
setWidth(observedDiv.current.offsetWidth)
if (observedDiv.current.offsetHeight !== height)
setHeight(observedDiv.current.offsetHeight)
})
resizeObserver.observe(observedDiv.current)
return function cleanup() {
resizeObserver.disconnect()
}
}, [width, height])
return (
<>
<Metadata title={`${process.env.NAME} | Contact`} />
<div className="flex min-h-[calc(100vh-6rem)] items-center justify-center">
<ContactCardCell />
</div>
</>
)
}
export default ContactPage

View File

@ -0,0 +1,5 @@
import PortraitCell from 'src/components/Portrait/PortraitCell'
const PortraitPage = () => <PortraitCell />
export default PortraitPage

View File

@ -1,7 +1,5 @@
import NewSocial from 'src/components/Social/NewSocial'
import NewSocial from 'src/components/Social/NewSocial/NewSocial'
const NewSocialPage = () => {
return <NewSocial />
}
const NewSocialPage = () => <NewSocial />
export default NewSocialPage

243
web/src/scaffold.css Normal file
View File

@ -0,0 +1,243 @@
.rw-scaffold {
@apply bg-white text-gray-600;
}
.rw-scaffold h1,
.rw-scaffold h2 {
@apply m-0;
}
.rw-scaffold a {
@apply bg-transparent;
}
.rw-scaffold ul {
@apply m-0 p-0;
}
.rw-scaffold input:-ms-input-placeholder {
@apply text-gray-500;
}
.rw-scaffold input::-ms-input-placeholder {
@apply text-gray-500;
}
.rw-scaffold input::placeholder {
@apply text-gray-500;
}
.rw-header {
@apply flex justify-between px-8 py-4;
}
.rw-main {
@apply mx-4 pb-4;
}
.rw-segment {
@apply w-full overflow-hidden rounded-lg border border-gray-200;
scrollbar-color: theme('colors.zinc.400') transparent;
}
.rw-segment::-webkit-scrollbar {
height: initial;
}
.rw-segment::-webkit-scrollbar-track {
@apply rounded-b-[10px] rounded-t-none border-0 border-t border-solid border-gray-200 bg-transparent p-[2px];
}
.rw-segment::-webkit-scrollbar-thumb {
@apply rounded-full border-[3px] border-solid border-transparent bg-zinc-400 bg-clip-content;
}
.rw-segment-header {
@apply bg-gray-200 px-4 py-3 text-gray-700;
}
.rw-segment-main {
@apply bg-gray-100 p-4;
}
.rw-link {
@apply text-blue-400 underline;
}
.rw-link:hover {
@apply text-blue-500;
}
.rw-forgot-link {
@apply mt-1 text-right text-xs text-gray-400 underline;
}
.rw-forgot-link:hover {
@apply text-blue-500;
}
.rw-heading {
@apply font-semibold;
}
.rw-heading.rw-heading-primary {
@apply text-xl;
}
.rw-heading.rw-heading-secondary {
@apply text-sm;
}
.rw-heading .rw-link {
@apply text-gray-600 no-underline;
}
.rw-heading .rw-link:hover {
@apply text-gray-900 underline;
}
.rw-cell-error {
@apply text-sm font-semibold;
}
.rw-form-wrapper {
@apply -mt-4 text-sm;
}
.rw-cell-error,
.rw-form-error-wrapper {
@apply my-4 rounded border border-red-100 bg-red-50 p-4 text-red-600;
}
.rw-form-error-title {
@apply m-0 font-semibold;
}
.rw-form-error-list {
@apply mt-2 list-inside list-disc;
}
.rw-button {
@apply flex cursor-pointer justify-center rounded border-0 bg-gray-200 px-4 py-1 text-xs font-semibold uppercase leading-loose tracking-wide text-gray-500 no-underline transition duration-100;
}
.rw-button:hover {
@apply bg-gray-500 text-white;
}
.rw-button.rw-button-small {
@apply rounded-sm px-2 py-1 text-xs;
}
.rw-button.rw-button-green {
@apply bg-green-500 text-white;
}
.rw-button.rw-button-green:hover {
@apply bg-green-700;
}
.rw-button.rw-button-blue {
@apply bg-blue-500 text-white;
}
.rw-button.rw-button-blue:hover {
@apply bg-blue-700;
}
.rw-button.rw-button-red {
@apply bg-red-500 text-white;
}
.rw-button.rw-button-red:hover {
@apply bg-red-700 text-white;
}
.rw-button-icon {
@apply mr-1 text-xl leading-5;
}
.rw-button-group {
@apply mx-2 my-3 flex justify-center;
}
.rw-button-group .rw-button {
@apply mx-1;
}
.rw-form-wrapper .rw-button-group {
@apply mt-8;
}
.rw-label {
@apply mt-6 block text-left font-semibold text-gray-600;
}
.rw-label.rw-label-error {
@apply text-red-600;
}
.rw-input {
@apply mt-2 block w-full rounded border border-gray-200 bg-white p-2 outline-none;
}
.rw-check-radio-items {
@apply flex justify-items-center;
}
.rw-check-radio-item-none {
@apply text-gray-600;
}
.rw-input[type='checkbox'],
.rw-input[type='radio'] {
@apply ml-0 mr-1 mt-1 inline w-4;
}
.rw-input:focus {
@apply border-gray-400;
}
.rw-input-error {
@apply border-red-600 text-red-600;
}
.rw-input-error:focus {
@apply border-red-600 outline-none;
box-shadow: 0 0 5px #c53030;
}
.rw-field-error {
@apply mt-1 block text-xs font-semibold uppercase text-red-600;
}
.rw-table-wrapper-responsive {
@apply overflow-x-auto;
}
.rw-table-wrapper-responsive .rw-table {
min-width: 48rem;
}
.rw-table {
@apply w-full text-sm;
}
.rw-table th,
.rw-table td {
@apply p-3;
}
.rw-table td {
@apply bg-white text-gray-900;
}
.rw-table tr:nth-child(odd) td,
.rw-table tr:nth-child(odd) th {
@apply bg-gray-50;
}
.rw-table thead tr {
@apply bg-gray-200 text-gray-600;
}
.rw-table th {
@apply text-left font-semibold;
}
.rw-table thead th {
@apply text-left;
}
.rw-table tbody th {
@apply text-right;
}
@media (min-width: 768px) {
.rw-table tbody th {
@apply w-1/5;
}
}
.rw-table tbody tr {
@apply border-t border-gray-200;
}
.rw-table input {
@apply ml-0;
}
.rw-table-actions {
@apply flex h-4 items-center justify-end pr-1;
}
.rw-table-actions .rw-button {
@apply bg-transparent;
}
.rw-table-actions .rw-button:hover {
@apply bg-gray-500 text-white;
}
.rw-table-actions .rw-button-blue {
@apply text-blue-500;
}
.rw-table-actions .rw-button-blue:hover {
@apply bg-blue-500 text-white;
}
.rw-table-actions .rw-button-red {
@apply text-red-600;
}
.rw-table-actions .rw-button-red:hover {
@apply bg-red-600 text-white;
}
.rw-text-center {
@apply text-center;
}
.rw-login-container {
@apply mx-auto my-16 flex w-96 flex-wrap items-center justify-center;
}
.rw-login-container .rw-form-wrapper {
@apply w-full text-center;
}
.rw-login-link {
@apply mt-4 w-full text-center text-sm text-gray-600;
}
.rw-webauthn-wrapper {
@apply mx-4 mt-6 leading-6;
}
.rw-webauthn-wrapper h2 {
@apply mb-4 text-xl font-bold;
}

View File

@ -6241,19 +6241,6 @@ __metadata:
languageName: node
linkType: hard
"@uppy/image-editor@npm:^3.0.0":
version: 3.0.0
resolution: "@uppy/image-editor@npm:3.0.0"
dependencies:
"@uppy/utils": "npm:^6.0.0"
cropperjs: "npm:1.5.7"
preact: "npm:^10.5.13"
peerDependencies:
"@uppy/core": ^4.0.0
checksum: 10c0/c609720ddb53c6116763a6f8e8569f05a6d67ed0e9664bdedcb65dca08a744d4bed2bcb141e8e701091c89a351cdf5bce28da57d0c39972a8a512ef200f063c7
languageName: node
linkType: hard
"@uppy/informer@npm:^4.0.0":
version: 4.0.0
resolution: "@uppy/informer@npm:4.0.0"
@ -9569,13 +9556,6 @@ __metadata:
languageName: node
linkType: hard
"cropperjs@npm:1.5.7":
version: 1.5.7
resolution: "cropperjs@npm:1.5.7"
checksum: 10c0/0674042b395397f17e8ffd5dcb639663bf00ff4dcbb896329033e7daa255f22ea5a61548460be9cf1c21e44b874d01d8464cde92bb7a9565670a24694b7468f1
languageName: node
linkType: hard
"cross-env@npm:7.0.3":
version: 7.0.3
resolution: "cross-env@npm:7.0.3"
@ -21651,7 +21631,6 @@ __metadata:
"@uppy/dashboard": "npm:^4.0.2"
"@uppy/drag-drop": "npm:^4.0.1"
"@uppy/file-input": "npm:^4.0.0"
"@uppy/image-editor": "npm:^3.0.0"
"@uppy/progress-bar": "npm:^4.0.0"
"@uppy/react": "npm:^4.0.1"
"@uppy/tus": "npm:^4.0.0"