mirror of
https://github.com/MAGICGrants/campaign-site.git
synced 2026-01-09 12:27:59 -05:00
Add register modal and some UI fixes
This commit is contained in:
17
components.json
Normal file
17
components.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "styles/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "components",
|
||||
"utils": "utils"
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ const CustomLink = ({
|
||||
|
||||
if (isInternalLink) {
|
||||
// @ts-ignore
|
||||
return <Link href={href} {...rest} />
|
||||
return <Link href={href} {...rest} />
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
import Link from './CustomLink'
|
||||
import MobileNav from './MobileNav'
|
||||
import ThemeSwitch from './ThemeSwitch'
|
||||
import headerNavLinks from '../data/headerNavLinks'
|
||||
import Logo from './Logo'
|
||||
import { Dialog, DialogTrigger } from './ui/dialog'
|
||||
import { Button } from './ui/button'
|
||||
import RegisterFormModal from './RegisterFormModal'
|
||||
|
||||
const Header = () => {
|
||||
const [registerIsOpen, setRegisterIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<header className="flex items-center justify-between py-10">
|
||||
<div>
|
||||
@@ -32,6 +39,13 @@ const Header = () => {
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>Register</Button>
|
||||
</DialogTrigger>
|
||||
<RegisterFormModal close={() => setRegisterIsOpen(false)} />
|
||||
</Dialog>
|
||||
</div>
|
||||
<ThemeSwitch />
|
||||
<MobileNav />
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
'use client'
|
||||
|
||||
import { useTheme } from 'next-themes'
|
||||
import { SVGProps } from 'react'
|
||||
|
||||
function Logo(props: SVGProps<SVGSVGElement>) {
|
||||
|
||||
145
components/RegisterFormModal.tsx
Normal file
145
components/RegisterFormModal.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from './ui/dialog'
|
||||
import { Input } from './ui/input'
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from './ui/form'
|
||||
import { Button } from './ui/button'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import { useToast } from './ui/use-toast'
|
||||
import { ReloadIcon } from '@radix-ui/react-icons'
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
confirmPassword: z.string().min(8),
|
||||
})
|
||||
.refine((data) => data.password === data.confirmPassword, {
|
||||
message: 'Passwords do not match.',
|
||||
path: ['confirmPassword'],
|
||||
})
|
||||
|
||||
type RegisterFormInputs = z.infer<typeof schema>
|
||||
|
||||
type Props = { close: () => void }
|
||||
|
||||
function RegisterFormModal({ close }: Props) {
|
||||
const { toast } = useToast()
|
||||
const form = useForm<RegisterFormInputs>({ resolver: zodResolver(schema) })
|
||||
const registerMutation = trpc.auth.register.useMutation()
|
||||
|
||||
async function onSubmit(data: RegisterFormInputs) {
|
||||
console.log(data)
|
||||
|
||||
try {
|
||||
await registerMutation.mutateAsync(data)
|
||||
|
||||
toast({
|
||||
title: 'Please check your email to verify your account.',
|
||||
})
|
||||
|
||||
close()
|
||||
} catch (error) {
|
||||
const errorMessage = (error as any).message
|
||||
|
||||
if (errorMessage === 'EMAIL_TAKEN') {
|
||||
return form.setError(
|
||||
'email',
|
||||
{ message: 'Email is already taken.' },
|
||||
{ shouldFocus: true }
|
||||
)
|
||||
}
|
||||
|
||||
toast({
|
||||
title: 'Sorry, something went wrong.',
|
||||
variant: 'destructive',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Register</DialogTitle>
|
||||
<DialogDescription>
|
||||
Start supporting Monero projects today!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="flex flex-col space-y-4"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="johndoe@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="confirmPassword"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Confirm password</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type="password" />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting && (
|
||||
<ReloadIcon className="mr-2 h-4 w-4 animate-spin" />
|
||||
)}{' '}
|
||||
Register
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
export default RegisterFormModal
|
||||
57
components/ui/button.tsx
Normal file
57
components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import * as React from 'react'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '../../utils/cn'
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
outline:
|
||||
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = 'Button'
|
||||
|
||||
export { Button, buttonVariants }
|
||||
117
components/ui/dialog.tsx
Normal file
117
components/ui/dialog.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as React from 'react'
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog'
|
||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
||||
|
||||
import { cn } from '../../utils/cn'
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-1.5 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = 'DialogHeader'
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = 'DialogFooter'
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
176
components/ui/form.tsx
Normal file
176
components/ui/form.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
} from 'react-hook-form'
|
||||
|
||||
import { cn } from '../../utils/cn'
|
||||
import { Label } from './label'
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const { getFieldState, formState } = useFormContext()
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error('useFormField should be used within <FormField>')
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn('space-y-2', className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = 'FormItem'
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && 'text-destructive', className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = 'FormLabel'
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = 'FormControl'
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn('text-[0.8rem] text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = 'FormDescription'
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn('text-[0.8rem] font-medium text-destructive', className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = 'FormMessage'
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
25
components/ui/input.tsx
Normal file
25
components/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from '../../utils/cn'
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = 'Input'
|
||||
|
||||
export { Input }
|
||||
24
components/ui/label.tsx
Normal file
24
components/ui/label.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as React from 'react'
|
||||
import * as LabelPrimitive from '@radix-ui/react-label'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '../../utils/cn'
|
||||
|
||||
const labelVariants = cva(
|
||||
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
|
||||
)
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
|
||||
export { Label }
|
||||
127
components/ui/toast.tsx
Normal file
127
components/ui/toast.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from 'react'
|
||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
||||
import * as ToastPrimitives from '@radix-ui/react-toast'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from '../../utils/cn'
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
'group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'border bg-background text-foreground',
|
||||
destructive:
|
||||
'destructive group border-destructive bg-destructive text-destructive-foreground',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600',
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<Cross2Icon className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn('text-sm font-semibold [&+div]:text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm opacity-90', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
33
components/ui/toaster.tsx
Normal file
33
components/ui/toaster.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
} from './toast'
|
||||
import { useToast } from './use-toast'
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
{toasts.map(function ({ id, title, description, action, ...props }) {
|
||||
return (
|
||||
<Toast key={id} {...props}>
|
||||
<div className="grid gap-1">
|
||||
{title && <ToastTitle>{title}</ToastTitle>}
|
||||
{description && (
|
||||
<ToastDescription>{description}</ToastDescription>
|
||||
)}
|
||||
</div>
|
||||
{action}
|
||||
<ToastClose />
|
||||
</Toast>
|
||||
)
|
||||
})}
|
||||
<ToastViewport />
|
||||
</ToastProvider>
|
||||
)
|
||||
}
|
||||
189
components/ui/use-toast.ts
Normal file
189
components/ui/use-toast.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// Inspired by react-hot-toast library
|
||||
import * as React from 'react'
|
||||
|
||||
import type { ToastActionElement, ToastProps } from './toast'
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: 'ADD_TOAST',
|
||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||
DISMISS_TOAST: 'DISMISS_TOAST',
|
||||
REMOVE_TOAST: 'REMOVE_TOAST',
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType['ADD_TOAST']
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType['UPDATE_TOAST']
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType['DISMISS_TOAST']
|
||||
toastId?: ToasterToast['id']
|
||||
}
|
||||
| {
|
||||
type: ActionType['REMOVE_TOAST']
|
||||
toastId?: ToasterToast['id']
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: 'REMOVE_TOAST',
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case 'ADD_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case 'UPDATE_TOAST':
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case 'DISMISS_TOAST': {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case 'REMOVE_TOAST':
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, 'id'>
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: 'UPDATE_TOAST',
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: 'ADD_TOAST',
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
33
env.mjs
Normal file
33
env.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
// src/env.mjs
|
||||
import { createEnv } from '@t3-oss/env-nextjs'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const env = createEnv({
|
||||
/*
|
||||
* Serverside Environment variables, not available on the client.
|
||||
* Will throw if you access these variables on the client.
|
||||
*/
|
||||
server: {
|
||||
OPEN_AI_API_KEY: z.string().min(1),
|
||||
},
|
||||
/*
|
||||
* Environment variables available on the client (and server).
|
||||
*
|
||||
* 💡 You'll get type errors if these are not prefixed with NEXT_PUBLIC_.
|
||||
*/
|
||||
client: {
|
||||
// NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string().min(1),
|
||||
},
|
||||
/*
|
||||
* Due to how Next.js bundles environment variables on Edge and Client,
|
||||
* we need to manually destructure them to make sure all are included in bundle.
|
||||
*
|
||||
* 💡 You'll get type errors if not all variables from `server` & `client` are included here.
|
||||
*/
|
||||
runtimeEnv: {
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
OPEN_AI_API_KEY: process.env.OPEN_AI_API_KEY,
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
||||
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||
},
|
||||
})
|
||||
1295
package-lock.json
generated
1295
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -14,19 +14,36 @@
|
||||
"@fortawesome/free-brands-svg-icons": "^6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"@hookform/resolvers": "^3.6.0",
|
||||
"@keycloak/keycloak-admin-client": "^24.0.5",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@sendgrid/mail": "^8.1.3",
|
||||
"@stripe/react-stripe-js": "^2.7.1",
|
||||
"@stripe/stripe-js": "^3.4.1",
|
||||
"@t3-oss/env-nextjs": "^0.10.1",
|
||||
"@tailwindcss/aspect-ratio": "^0.4.2",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tanstack/react-query": "^5.40.1",
|
||||
"@trpc/client": "^11.0.0-rc.390",
|
||||
"@trpc/next": "^11.0.0-rc.390",
|
||||
"@trpc/react-query": "^11.0.0-rc.390",
|
||||
"@trpc/server": "^11.0.0-rc.390",
|
||||
"@types/escape-html": "^1.0.4",
|
||||
"base-64": "^1.0.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"classnames": "^2.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"escape-html": "^1.0.3",
|
||||
"exclude": "^1.0.0",
|
||||
"fs": "^0.0.1-security",
|
||||
"gray-matter": "^4.0.3",
|
||||
"lucide-react": "^0.390.0",
|
||||
"micro": "^10.0.1",
|
||||
"micro-cors": "^0.1.1",
|
||||
"next": "^14.2.3",
|
||||
@@ -42,10 +59,13 @@
|
||||
"sharp": "^0.33.4",
|
||||
"stripe": "^15.9.0",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typed.js": "^2.1.0",
|
||||
"watch": "^0.13.0",
|
||||
"wicg-inert": "^3.1.2",
|
||||
"xss": "^1.0.15"
|
||||
"xss": "^1.0.15",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^8.1.0",
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
import type { AppProps } from 'next/app'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
import '../styles/globals.css'
|
||||
import '../styles/tailwind.css'
|
||||
|
||||
import Layout from '../components/Layout'
|
||||
import Head from 'next/head'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
import Layout from '../components/Layout'
|
||||
import { trpc } from '../utils/trpc'
|
||||
|
||||
import '../styles/globals.css'
|
||||
import { Toaster } from '../components/ui/toaster'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" defaultTheme='system'>
|
||||
<ThemeProvider attribute="class" defaultTheme="system">
|
||||
<Head>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport" />
|
||||
</Head>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
export default trpc.withTRPC(MyApp)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import xss from 'xss'
|
||||
|
||||
import markdownToHtml from '../utils/markdownToHtml'
|
||||
import { getSingleFile } from '../utils/md'
|
||||
import BigDumbMarkdown from '../components/BigDumbMarkdown'
|
||||
import xss from 'xss'
|
||||
|
||||
export default function About({ content }: { content: string }) {
|
||||
return (
|
||||
|
||||
9
pages/api/trpc/[trpc].ts
Normal file
9
pages/api/trpc/[trpc].ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import * as trpcNext from '@trpc/server/adapters/next'
|
||||
import { appRouter } from '../../../server/routers/_app'
|
||||
|
||||
// export API handler
|
||||
// @link https://trpc.io/docs/v11/server/adapters
|
||||
export default trpcNext.createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext: () => ({}),
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import { useState } from 'react'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { fetchPostJSON } from '../utils/api-helpers'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '../components/ui/button'
|
||||
export default function Apply() {
|
||||
async function handleClick() {}
|
||||
const [loading, setLoading] = useState(false)
|
||||
@@ -29,7 +30,7 @@ export default function Apply() {
|
||||
<div className="mx-auto flex-1 flex flex-col items-center justify-center gap-4 py-8 prose dark:prose-dark">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="apply flex flex-col gap-4 p-4 max-w-2xl"
|
||||
className="max-w-5xl flex flex-col gap-4 p-4"
|
||||
>
|
||||
<div>
|
||||
<h1>
|
||||
@@ -296,12 +297,7 @@ export default function Apply() {
|
||||
need to identify themselves to MAGIC, in accordance with US law.
|
||||
</small>
|
||||
|
||||
<button
|
||||
className="mb-2 mr-2 w-full rounded bg-orange-500 px-4 text-xl font-semibold text-white hover:border-transparent hover:bg-orange-500 hover:text-black dark:text-black dark:hover:text-white md:max-w-[98%] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={loading}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
<Button disabled={loading}>Apply</Button>
|
||||
|
||||
<p>
|
||||
After submitting your application, please allow our team up to three
|
||||
|
||||
@@ -4,14 +4,13 @@ import { useEffect, useState } from 'react'
|
||||
import ProjectList from '../components/ProjectList'
|
||||
import PaymentModal from '../components/PaymentModal'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import magiclogo from '/public/img/crystalball.jpg'
|
||||
|
||||
import { getAllPosts } from '../utils/md'
|
||||
import { ProjectItem } from '../utils/types'
|
||||
import { useRouter } from 'next/router'
|
||||
import Typing from '../components/Typing'
|
||||
import CustomLink from '../components/CustomLink'
|
||||
import { Button } from '../components/ui/button'
|
||||
|
||||
// These shouldn't be swept up in the regular list so we hardcode them
|
||||
const generalFund: ProjectItem = {
|
||||
@@ -73,12 +72,13 @@ const Home: NextPage<{ projects: any }> = ({ projects }) => {
|
||||
</p>
|
||||
<div className="flex flex-wrap py-4">
|
||||
<div className="w-full md:w-1/2">
|
||||
<button
|
||||
<Button
|
||||
onClick={openGeneralFundModal}
|
||||
className="mb-2 mr-2 w-full rounded bg-orange-500 px-4 text-xl font-semibold text-white hover:border-transparent hover:bg-orange-500 hover:text-black dark:text-black dark:hover:text-white md:max-w-[98%]"
|
||||
size="lg"
|
||||
className="px-14 text-black font-semibold text-xl"
|
||||
>
|
||||
Donate to Monero Comittee General Fund
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-lg leading-7 text-gray-500 dark:text-gray-400">
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function ThankYou() {
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4 py-8">
|
||||
<h2>Thank you for your donation!</h2>
|
||||
<p>
|
||||
If you have any questions, please reach out to{' '}
|
||||
<a href="mailto:info@magicgrants.org">info@magicgrants.org</a>
|
||||
</p>
|
||||
.
|
||||
<p>
|
||||
<Link href="/">Return Home</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-4 py-8">
|
||||
<h2>Thank you for your donation!</h2>
|
||||
<p>
|
||||
If you have any questions, please reach out to{' '}
|
||||
<a
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="mailto:info@magicgrants.org"
|
||||
>
|
||||
info@magicgrants.org
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
|
||||
<p>
|
||||
<Link
|
||||
className="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
|
||||
href="/"
|
||||
>
|
||||
Return Home
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
8
server/routers/_app.ts
Normal file
8
server/routers/_app.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { mergeRouters, router } from '../trpc'
|
||||
import { authRouter } from './auth'
|
||||
|
||||
export const appRouter = router({
|
||||
auth: authRouter,
|
||||
})
|
||||
|
||||
export type AppRouter = typeof appRouter
|
||||
44
server/routers/auth.ts
Normal file
44
server/routers/auth.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { z } from 'zod'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { publicProcedure, router } from '../trpc'
|
||||
import { authenticateKeycloakClient } from '../utils/keycloak'
|
||||
import { keycloak } from '../services'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
|
||||
export const authRouter = router({
|
||||
register: publicProcedure
|
||||
.input(z.object({ email: z.string().email(), password: z.string() }))
|
||||
.mutation(async ({ input }) => {
|
||||
console.log(input)
|
||||
await authenticateKeycloakClient()
|
||||
|
||||
try {
|
||||
const user = await keycloak.users.create({
|
||||
realm: 'monerofund',
|
||||
email: input.email,
|
||||
credentials: [
|
||||
{ type: 'password', value: input.password, temporary: false },
|
||||
],
|
||||
requiredActions: ['VERIFY_EMAIL'],
|
||||
enabled: true,
|
||||
})
|
||||
|
||||
await keycloak.users.executeActionsEmail({
|
||||
id: user.id,
|
||||
actions: ['VERIFY_EMAIL'],
|
||||
})
|
||||
} catch (error) {
|
||||
if (
|
||||
(error as any).responseData.errorMessage ===
|
||||
'User exists with same email'
|
||||
) {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'EMAIL_TAKEN' })
|
||||
}
|
||||
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'UNKNOWN_ERROR',
|
||||
})
|
||||
}
|
||||
}),
|
||||
})
|
||||
6
server/services.ts
Normal file
6
server/services.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import KeycloakAdminClient from '@keycloak/keycloak-admin-client'
|
||||
|
||||
export const keycloak = new KeycloakAdminClient({
|
||||
baseUrl: 'http://localhost:8080',
|
||||
realmName: 'monerofund',
|
||||
})
|
||||
12
server/trpc.ts
Normal file
12
server/trpc.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { initTRPC } from '@trpc/server'
|
||||
|
||||
// Avoid exporting the entire t-object
|
||||
// since it's not very descriptive.
|
||||
// For instance, the use of a t variable
|
||||
// is common in i18n libraries.
|
||||
const t = initTRPC.create()
|
||||
|
||||
// Base router and procedure helpers
|
||||
export const router = t.router
|
||||
export const publicProcedure = t.procedure
|
||||
export const mergeRouters = t.mergeRouters
|
||||
8
server/utils/keycloak.ts
Normal file
8
server/utils/keycloak.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { keycloak } from '../services'
|
||||
|
||||
export const authenticateKeycloakClient = () =>
|
||||
keycloak.auth({
|
||||
clientId: 'app',
|
||||
clientSecret: '7JryN6EVIYtCwN4iHheacjp986Rfy5FJ',
|
||||
grantType: 'client_credentials',
|
||||
})
|
||||
@@ -1,34 +1,139 @@
|
||||
button.small {
|
||||
@apply p-1 text-sm bg-white text-black border border-black;
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
|
||||
--primary: 25 95% 53%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 222.2 84% 4.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0, 0%, 9%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
--card: 25 95% 53%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
--primary: 25 95% 53%;
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 0 0% 60%;
|
||||
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
--border: 0 0% 20%;
|
||||
--input: 0 0% 20%;
|
||||
--ring: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
button.group {
|
||||
@apply p-2 text-sm bg-white text-black border border-black;
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
}
|
||||
|
||||
button.pay {
|
||||
@apply rounded border border-orange-500 bg-transparent font-semibold text-orange-500 hover:border-transparent hover:bg-orange-500 hover:text-white flex-1 min-w-[20rem] border-2 border-black bg-white p-8 text-xl rounded-xl flex justify-start gap-4;
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
}
|
||||
|
||||
button.pay:disabled {
|
||||
@apply bg-gray-100 border-gray-400 text-gray-400 hover:shadow-none;
|
||||
.task-list-item::before {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
@apply mr-2;
|
||||
.task-list-item {
|
||||
@apply list-none;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-bottom: 56.25%;
|
||||
.footnotes {
|
||||
@apply mt-12 border-t border-gray-200 pt-8 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.video-container iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
.data-footnote-backref {
|
||||
@apply no-underline;
|
||||
}
|
||||
|
||||
.csl-entry {
|
||||
@apply my-5;
|
||||
}
|
||||
|
||||
/* https://stackoverflow.com/questions/61083813/how-to-avoid-internal-autofill-selected-style-to-be-applied */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:focus {
|
||||
transition:
|
||||
background-color 600000s 0s,
|
||||
color 600000s 0s;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
h1 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
h2 {
|
||||
@apply text-xl;
|
||||
}
|
||||
h3 {
|
||||
@apply text-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.prose > ul > li > p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.prose small {
|
||||
@apply leading-normal;
|
||||
}
|
||||
|
||||
form label {
|
||||
@apply text-sm font-semibold leading-normal;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
form input[type='text'],
|
||||
form textarea {
|
||||
@apply mt-2;
|
||||
}
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.task-list-item::before {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
.task-list-item {
|
||||
@apply list-none;
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
@apply mt-12 border-t border-gray-200 pt-8 dark:border-gray-700;
|
||||
}
|
||||
|
||||
.data-footnote-backref {
|
||||
@apply no-underline;
|
||||
}
|
||||
|
||||
.csl-entry {
|
||||
@apply my-5;
|
||||
}
|
||||
|
||||
/* https://stackoverflow.com/questions/61083813/how-to-avoid-internal-autofill-selected-style-to-be-applied */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:focus {
|
||||
transition:
|
||||
background-color 600000s 0s,
|
||||
color 600000s 0s;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
h1 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
h2 {
|
||||
@apply text-xl;
|
||||
}
|
||||
h3 {
|
||||
@apply text-lg;
|
||||
}
|
||||
}
|
||||
|
||||
button.small {
|
||||
@apply p-1 text-sm bg-white text-black border border-black;
|
||||
}
|
||||
|
||||
button.group {
|
||||
@apply p-2 text-sm bg-white text-black border border-black;
|
||||
}
|
||||
|
||||
button.pay {
|
||||
@apply flex-1 min-w-[20rem] border-2 border-black bg-white p-8 text-xl text-black rounded-xl flex justify-start gap-4;
|
||||
}
|
||||
|
||||
button.pay:disabled {
|
||||
@apply bg-gray-100 border-gray-400 text-gray-400 hover:shadow-none;
|
||||
}
|
||||
|
||||
button,
|
||||
button[type='submit'] {
|
||||
@apply py-2 px-4 text-white rounded;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
@apply bg-white text-black border;
|
||||
}
|
||||
|
||||
.prose > ul > li > p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.prose small {
|
||||
@apply leading-normal;
|
||||
}
|
||||
|
||||
form label {
|
||||
@apply text-sm font-semibold leading-normal;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@apply flex items-center;
|
||||
}
|
||||
|
||||
form input[type='text'],
|
||||
form textarea {
|
||||
@apply mt-2;
|
||||
}
|
||||
@@ -1,47 +1,85 @@
|
||||
// @ts-check
|
||||
const { fontFamily } = require('tailwindcss/defaultTheme')
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
// ../node_modules/pliny/dist/**/*.mjs is needed for monorepo setup
|
||||
/** @type {import("tailwindcss/types").Config } */
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
'../node_modules/pliny/**/*.{js,ts,tsx}',
|
||||
'./node_modules/pliny/**/*.{js,ts,tsx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx}',
|
||||
'./pages/**/*.{js,ts,tsx}',
|
||||
'./components/**/*.{js,ts,tsx}',
|
||||
'./layouts/**/*.{js,ts,tsx}',
|
||||
'./lib/**/*.{js,ts,tsx}',
|
||||
'./data/**/*.mdx',
|
||||
'./pages/**/*.{ts,tsx}',
|
||||
'./components/**/*.{ts,tsx}',
|
||||
'./app/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
],
|
||||
darkMode: 'class',
|
||||
prefix: '',
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: '2rem',
|
||||
screens: {
|
||||
'2xl': '1400px',
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
spacing: {
|
||||
'9/16': '56.25%',
|
||||
},
|
||||
lineHeight: {
|
||||
11: '2.75rem',
|
||||
12: '3rem',
|
||||
13: '3.25rem',
|
||||
14: '3.5rem',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', ...fontFamily.sans],
|
||||
},
|
||||
colors: {
|
||||
primary: colors.orange,
|
||||
gray: colors.neutral,
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
DEFAULT_HOVER: 'hsl(var(--primary) / 0.7)',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: { height: '0' },
|
||||
to: { height: 'var(--radix-accordion-content-height)' },
|
||||
},
|
||||
'accordion-up': {
|
||||
from: { height: 'var(--radix-accordion-content-height)' },
|
||||
to: { height: '0' },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
typography: (theme) => ({
|
||||
DEFAULT: {
|
||||
css: {
|
||||
color: theme('colors.gray.700'),
|
||||
a: {
|
||||
color: theme('colors.primary.500'),
|
||||
color: theme('colors.primary.DEFAULT'),
|
||||
'&:hover': {
|
||||
color: `${theme('colors.primary.600')} !important`,
|
||||
color: `${theme('colors.primary.DEFAULT_HOVER')} !important`,
|
||||
},
|
||||
code: { color: theme('colors.primary.400') },
|
||||
},
|
||||
@@ -107,9 +145,9 @@ module.exports = {
|
||||
css: {
|
||||
color: theme('colors.gray.300'),
|
||||
a: {
|
||||
color: theme('colors.primary.500'),
|
||||
color: theme('colors.primary.DEFAULT'),
|
||||
'&:hover': {
|
||||
color: `${theme('colors.primary.400')} !important`,
|
||||
color: `${theme('colors.primary.DEFAULT_HOVER')} !important`,
|
||||
},
|
||||
code: { color: theme('colors.primary.400') },
|
||||
},
|
||||
@@ -168,7 +206,7 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require('@tailwindcss/forms'),
|
||||
require('tailwindcss-animate'),
|
||||
require('@tailwindcss/typography'),
|
||||
require('@tailwindcss/aspect-ratio'),
|
||||
],
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -15,6 +20,12 @@
|
||||
"jsx": "preserve",
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
6
utils/cn.ts
Normal file
6
utils/cn.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from 'clsx'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
47
utils/trpc.ts
Normal file
47
utils/trpc.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { httpBatchLink } from '@trpc/client'
|
||||
import { createTRPCNext } from '@trpc/next'
|
||||
import type { AppRouter } from '../server/routers/_app'
|
||||
|
||||
function getBaseUrl() {
|
||||
if (typeof window !== 'undefined')
|
||||
// browser should use relative path
|
||||
return ''
|
||||
|
||||
if (process.env.VERCEL_URL)
|
||||
// reference for vercel.com
|
||||
return `https://${process.env.VERCEL_URL}`
|
||||
|
||||
if (process.env.RENDER_INTERNAL_HOSTNAME)
|
||||
// reference for render.com
|
||||
return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`
|
||||
|
||||
// assume localhost
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`
|
||||
}
|
||||
|
||||
export const trpc = createTRPCNext<AppRouter>({
|
||||
config(opts) {
|
||||
return {
|
||||
links: [
|
||||
httpBatchLink({
|
||||
/**
|
||||
* If you want to use SSR, you need to use the server's full URL
|
||||
* @link https://trpc.io/docs/v11/ssr
|
||||
**/
|
||||
url: `${getBaseUrl()}/api/trpc`,
|
||||
|
||||
// You can pass any HTTP headers you wish here
|
||||
async headers() {
|
||||
return {
|
||||
// authorization: getAuthCookie(),
|
||||
}
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @link https://trpc.io/docs/v11/ssr
|
||||
**/
|
||||
ssr: false,
|
||||
})
|
||||
Reference in New Issue
Block a user