Contact Page
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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} />
|
||||
|
||||
80
web/src/components/ContactCard/ContactCard/ContactCard.tsx
Normal file
80
web/src/components/ContactCard/ContactCard/ContactCard.tsx
Normal 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
|
||||
@@ -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} />
|
||||
)
|
||||
37
web/src/components/Portrait/PortraitCell/PortraitCell.tsx
Normal file
37
web/src/components/Portrait/PortraitCell/PortraitCell.tsx
Normal 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} />
|
||||
213
web/src/components/Portrait/PortraitForm/PortraitForm.tsx
Normal file
213
web/src/components/Portrait/PortraitForm/PortraitForm.tsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
26
web/src/components/Social/SocialLinks/SocialLinks.tsx
Normal file
26
web/src/components/Social/SocialLinks/SocialLinks.tsx
Normal 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
|
||||
@@ -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} />
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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 = () =>
|
||||
|
||||
42
web/src/pages/ContactPage/ContactPage.tsx
Normal file
42
web/src/pages/ContactPage/ContactPage.tsx
Normal 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
|
||||
5
web/src/pages/Portrait/PortraitPage/PortraitPage.tsx
Normal file
5
web/src/pages/Portrait/PortraitPage/PortraitPage.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import PortraitCell from 'src/components/Portrait/PortraitCell'
|
||||
|
||||
const PortraitPage = () => <PortraitCell />
|
||||
|
||||
export default PortraitPage
|
||||
@@ -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
243
web/src/scaffold.css
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user