Campaign Site V2 (#81)

* Apply OpenSats UI enhancements

* Add register modal and some UI fixes

* Add login modal

* Add reset password button

* Use @t3-oss/env-nextjs for env variables

* Email verification without keycloak UI

* Password reset without keycloak UI

* Add "My donations" page with one-time stripe donations list

* Display crypto donations in "My donations" page

* Donation form fixes and improvements

* Display recurring annual fiat donations

* Include keycloak realm export file and remove hardcoded client values

* fix: correctly handle btcpay webhooks and fix donationList query

* feat: add membership modal and implement membership payment using btcpay

* feat: add procedure for membership purchases with stripe and use db as single source of truth for donations

* feat: use webhooks to update stripe donation/membership status

* feat: memberships list page and fixes

* feat: db schema changes and webhook fixes

* feat: re-add "donate" and add "get annual membership" buttons to project page

* feat: open register modal when clicking membership button while logged out

* feat: replace "Get Membership" button with "My Memberships" button when user already has a membership for that project

* feat: multiple funds support

* deps: bump axios

* feat: add different color schemes for each fund and some fixes

* feat: add home page

* feat: add missing titles and responsiveness improvements

* chore: add prod workflow file and compose file

* chore(deploy workflow): set environment name

* chore(Dockerfile): add necessary lines for prisma

* chore: make it skip env validation on build

* fix: prevent donation amounts from being fetched from db during build

* chore(nginx.conf): remove copy-paste junk

* chore(docker compose): correctly set APP_URL env

* feat: replace Sendgrid with SES

* deps: audit fix

* chore(deploy.yml): remove unecessary env

* fix: correctly manage client and server env

* fix(trpc.submitApplication): get recipient emails from server side env

* fix(Dockerfile): define NEXT_PUBLIC_ env on build

* chore(trpc): make it log any errors

* chore(trpc): improve displaying of errors

* chore(trpc): improve displaying of errors

* fix: buggy link buttons

* feat: use single btcpay store

* feat: show form 8283 info in donation form and handle tax deductible donations

* feat: have only one privacy and terms page for the entire site

* feat: support many social links

* feat: add funding required endpoint (wip)

* feat: get rates using btcpay api and small refactor

* deps: audit fix

* fix: correctly handle payment methods on InvoiceSettled event

* fix: make index on Donation.btcPayInvoiceId

* feat(funding-required): improve asset parameter response

* feat(funding-required): add project_status param

* feat(funding-required): add fund param

* feat(funding-required): implement caching

* fix(funding-required): minor fixes

* feat(funding-required): add remaining_amount_<currency> fields and fixes

* chore: include all services in docker-compose.dev.yml and update .env.example

* feat: move terms and privacy links to footer

* fix: address font not always loading bug

* feat: use fund logos as header image

* feat: donation confirmation email

* fix: use correct stripe client for each fund on webhooks

* feat: add account settings page with change password form

* feat: add email change form to settings page

* fix: address wrong btcpay invoice url redirect

* chore: email change request debug

* fix(api): better handle user attributes

* feat: ui improvements

* feat: add btcpay invoice item description

* chore(nginx): api rate limit

* feat: remove typing component from fund landing pages

* feat: implement refresh token rotation using keycloak

* refactor: have gross and net amounts for donations

* feat: invalidate user sessions on password/email change

* fix: make "Create an account" button work on donate/membership modals

* refactor: project props

* fix(utils.md): correctly load md project attributes

* chore(prisma): make composite unique constraint for fundSlug and projectSlug on ProjectAddresses

* chore: mark example project as not funded

* fix(utils.md): serialization error

* chore(funding-required): btcpay invoice payment methods debug

* fix(funding-required): get bitcoin address from correct payment method

* fix(funding-required): correctly handle project_status ANY filter

* fix(btcpay webhook handler): correctly handle payment methods on InvoicePaymentSettled

* chore(docker-compose.yml): expose nginx port 80

* fix(funding-required): correctly concat project url

* feat: ui improvements for smaller screens

* fix(btcpay webhook handler): correctly get payment method amount on InvoiceSettled

* fix(btcpay webhook handler): respond with 200 immediately if there is no metadata

* chore(funding-required): debugging

* chore(funding-required): debugging

* chore(funding-required): debugging

* fix(Dockerfile): define BUILD_MODE as arg instead of env to make it blank at runtime

* fix: correctly pass current and goal values to project card progress

* fix(funding-required): set high monitoring time for static address invoice

* fix: correctly handle refresh token expiration on the ui

* feat: ui improvements

* chore: update README

* chore: update README

* Initial site text

* fix: colors

* chore: mention funds accordingly in texts

* chore: update realm-export.json

* chore: rename docker compose files

* Update emails

* Form updates

* Remove unused pages and page improvements

* feat: allow editing navbar links for each fund

* Cleanup and Firo projects

* chore(deploy.yml): change deploy branch to master

* fix(auth): use fetch instead of axios when fetching refresh token due to edge runtime compatibility

* fix: keep empty project folders

* Fix code scanning alert no. 20: DOM text reinterpreted as HTML

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* chore: sanitize md file paths

* Text and link updates

---------

Co-authored-by: Artur N <arturnunespe@gmail.com>
Co-authored-by: Justin Ehrenhofer <12520755+SamsungGalaxyPlayer@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
Artur
2024-10-17 12:29:40 -03:00
committed by GitHub
parent f7453578b5
commit 82955be4cb
161 changed files with 18657 additions and 8633 deletions

38
.env.example Normal file
View File

@@ -0,0 +1,38 @@
APP_URL="http://localhost:3000"
DATABASE_URL="postgresql://magic:magic@magic-postgres:5432/magic?schema=public"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_URL_INTERNAL="http://localhost:3000"
NEXTAUTH_SECRET=""
USER_SETTINGS_JWT_SECRET=""
SMTP_HOST="sandbox.smtp.mailtrap.io"
SMTP_PORT="2525"
SMTP_USER=""
SMTP_PASS=""
SES_VERIFIED_SENDER=""
STRIPE_MONERO_SECRET_KEY=""
STRIPE_MONERO_WEBHOOK_SECRET=""
STRIPE_FIRO_SECRET_KEY=""
STRIPE_FIRO_WEBHOOK_SECRET=""
STRIPE_PRIVACY_GUIDES_SECRET_KEY=""
STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET=""
STRIPE_GENERAL_SECRET_KEY=""
STRIPE_GENERAL_WEBHOOK_SECRET=""
BTCPAY_URL="http://btcpayserver:49392"
BTCPAY_EXTERNAL_URL="http://localhost:49392"
BTCPAY_API_KEY=""
BTCPAY_STORE_ID=""
BTCPAY_WEBHOOK_SECRET=""
KEYCLOAK_URL="http://keycloak:8080"
KEYCLOAK_CLIENT_ID="app"
KEYCLOAK_CLIENT_SECRET=""
KEYCLOAK_REALM_NAME="magic"
MONERO_APPLICATION_RECIPIENT=""
FIRO_APPLICATION_RECIPIENT=""
PRIVACY_GUIDES_APPLICATION_RECIPIENT=""
GENERAL_APPLICATION_RECIPIENT=""

View File

@@ -1,3 +1,6 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"rules": {
"react-hooks/exhaustive-deps": "off"
}
}

50
.github/workflows/deploy.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Deploy app to donate.magicgrants.org
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
environment: master
steps:
- uses: actions/checkout@v4
- uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Deploy
run: |
ssh -o StrictHostKeyChecking=no ${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }} << 'EOF'
export HISTFILE=/dev/null
cd campaign-site
git checkout v2
echo "Pulling changes..."
git pull
echo "Building and starting..."
CLOUDFLARE_TUNNEL_TOKEN=${{ secrets.CLOUDFLARE_TUNNEL_TOKEN }} \
POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} \
DATABASE_URL=${{ secrets.DATABASE_URL }} \
NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }} \
USER_SETTINGS_JWT_SECRET=${{ secrets.USER_SETTINGS_JWT_SECRET }} \
SMTP_USER=${{ secrets.SMTP_USER }} \
SMTP_PASS=${{ secrets.SMTP_PASS }} \
STRIPE_MONERO_SECRET_KEY=${{ secrets.STRIPE_MONERO_SECRET_KEY }} \
STRIPE_MONERO_WEBHOOK_SECRET=${{ secrets.STRIPE_MONERO_WEBHOOK_SECRET }} \
STRIPE_FIRO_SECRET_KEY=${{ secrets.STRIPE_FIRO_SECRET_KEY }} \
STRIPE_FIRO_WEBHOOK_SECRET=${{ secrets.STRIPE_FIRO_WEBHOOK_SECRET }} \
STRIPE_PRIVACY_GUIDES_SECRET_KEY=${{ secrets.STRIPE_PRIVACY_GUIDES_SECRET_KEY }} \
STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET=${{ secrets.STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET }} \
STRIPE_GENERAL_SECRET_KEY=${{ secrets.STRIPE_GENERAL_SECRET_KEY }} \
STRIPE_GENERAL_WEBHOOK_SECRET=${{ secrets.STRIPE_GENERAL_WEBHOOK_SECRET }} \
KEYCLOAK_URL=${{ secrets.KEYCLOAK_URL }} \
KEYCLOAK_CLIENT_SECRET=${{ secrets.KEYCLOAK_CLIENT_SECRET }} \
BTCPAY_URL=${{ secrets.BTCPAY_URL }} \
BTCPAY_API_KEY=${{ secrets.BTCPAY_API_KEY }} \
BTCPAY_STORE_ID=${{ secrets.BTCPAY_STORE_ID }} \
BTCPAY_WEBHOOK_SECRET=${{ secrets.BTCPAY_WEBHOOK_SECRET }} \
docker compose -f docker-compose.prod.yml up -d --build
EOF

View File

@@ -2,7 +2,7 @@ module.exports = {
semi: false,
trailingComma: 'es5',
singleQuote: true,
printWidth: 80,
printWidth: 100,
tabWidth: 2,
useTabs: false,
jsxBracketSameLine: false,

View File

@@ -6,6 +6,9 @@ FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY prisma prisma
ENV PRISMA_BINARY_TARGETS='["native", "rhel-openssl-1.0.x"]'
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
@@ -26,6 +29,13 @@ COPY . .
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
ENV NEXT_TELEMETRY_DISABLED 1
ENV BUILD_MODE 1
ENV PRISMA_BINARY_TARGETS='["native", "rhel-openssl-1.0.x"]'
ENV NEXT_PUBLIC_MONERO_APPLICATION_RECIPIENT='monerofund@magicgrants.org'
ENV NEXT_PUBLIC_FIRO_APPLICATION_RECIPIENT='firofund@magicgrants.org'
ENV NEXT_PUBLIC_PRIVACY_GUIDES_APPLICATION_RECIPIENT='privacyguidesfund@magicgrants.org'
ENV NEXT_PUBLIC_GENERAL_APPLICATION_RECIPIENT='info@magicgrants.org'
RUN npx prisma generate
RUN \
if [ -f yarn.lock ]; then yarn run build; \
@@ -38,6 +48,8 @@ RUN \
FROM base AS runner
WORKDIR /app
ARG BUILD_MODE=1
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
ENV NEXT_TELEMETRY_DISABLED 1
@@ -45,6 +57,7 @@ ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
@@ -63,7 +76,16 @@ WORKDIR /app
EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"
# Install Prisma CLI
RUN mkdir /home/nextjs/.npm-global
ENV PATH=/home/nextjs/.npm-global/bin:$PATH
ENV NPM_CONFIG_PREFIX=/home/nextjs/.npm-global
ENV PRISMA_BINARY_TARGETS='["native", "rhel-openssl-1.0.x"]'
RUN npm install --quiet --no-progress -g prisma
RUN npm cache clean --force
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
CMD HOSTNAME="0.0.0.0" node server.js
CMD ["/bin/sh", "-c", "prisma migrate deploy && node server.js"]

128
README.md
View File

@@ -1,19 +1,127 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# MAGIC Grants Campaign Site
PRs welcome!
## Development
Thanks for supporting MAGIC Monero Fund.
### Requirements
---
- Docker
- Docker Compose
- NodeJS >=20
# Upgrading Server
### Running containers
ssh into the machine, and cd into the correct folder, under Production. Then:
First, install the application's dependencies and then run the containers:
`sudo git pull`
```bash
$ npm i
```
`sudo docker-compose up --build`
```bash
$ docker-compse up -d
```
After building coompletes, quit out with Ctrl + C
The app will be available at `http://localhost:3000`.
`sudo docker start monerofund-frontend-page`
### Configuration
Create a `.env `file as a copy of `.env.example` and set the values for the empty variables.
### Setting up Keycloak
1. Open up http://localhost:8080 in your browser, then login using `admin` for both username and password;
2. Open the dropdown menu on the top left corner of the screen (where it says Keycloak) and click **Create realm**;
3. Upload the `realm-export.json` file from this repo, name the realm `magic` and click **Create**;
4. Once the realm is created, go to **Clients** -> **Credentials**, and under **Client Secret**, click **Regenerate**. Copy the secret and add it to the `KEYCLOAK_CLIENT_SECRET` environment variable in your `.env` file.
### Setting up BTCPayServer
1. Open up http://localhost:49392 in your browser and create an account;
2. Once logged in, open the **XMR Wallet** page and upload a view-only wallet file, you can get one from [Feather Wallet](https://featherwallet.org/);
3. In the **Webhooks** tab, create a new webhook by setting the **Payload URL** to `http://campaign-site:3000/api/btcpay/webhook`, copy the secret and add it to the `BTCPAY_WEBHOOK_SECRET` environment variable in your `.env` file, and finally click **Add webhook**.
4. Create a new API key at **Account** -> **Manage Account** -> **API Keys**, you'll need the following permissions: **View invoices**, **Create an invoice** and **View your stores**. Then copy the API key and add it to the `BTCPAY_API_KEY` environment variable in your `.env` file.
### Setting up Stripe
1. Open up the [Stripe Dashboard](https://dashboard.stripe.com) in your browser;
2. Create a new account in test mode;
3. Go to **Developers** -> **API keys**, and get a secret key, add it to the `STRIPE_MONERO_SECRET_KEY` environment variable in your `.env` file;
4. Go to **Developers** -> **Webhooks**, and add a new webhook endpoint. Add `<Your app public address>/api/stripe/monero-webhook` to the URL field replacing `<Your app public address>` with your app's public address, then add the secret to the `STRIPE_MONERO_WEBHOOK_SECRET` environment variable in your `.env` file. To expose the app to the internet so Stripe can reach the webhook endpoint, you can use tunneling services like Visual Studio Code's built-in [port-forwarding feature](https://code.visualstudio.com/docs/editor/port-forwarding) or [Ngrok](https://ngrok.io).
This makes you able to test Stripe donations in the Monero fund, which should be enough for development. You can add random values to the other Stripe enviroment variables to bypass validation.
You are now all set up to start developing!
## Funding required endpoint
Endpoint: `GET /api/funding-required`
Request query parameters
| Parameter | Required | Default | Accepted values | Description |
| - | - | - | - | - |
| `fund` | No | - | `monero` `firo` `privacyguides` `general` | Filters projects by fund. |
| `asset` | No | - | `BTC` `XMR` `USD` | Only return project amounts and address for a specific asset. Specifying this parameter changes the response JSON schema as shown below. |
| `project_status` | No | `NOT_FUNDED` | `NOT_FUNDED` `FUNDED` `ANY` | Filters projects by status. |
Response body (`asset` parameter **not** specified)
```ts
[
{
title: string
fund: 'monero' | 'firo' | 'privacyguides' | 'general'
date: string // YYYY-MM-DD
author: string
url: string
is_funded: boolean
raised_amount_percent: number
contributions: number
target_amount_btc: number
target_amount_xmr: number
target_amount_usd: number
remaining_amount_btc: number
remaining_amount_xmr: number
remaining_amount_usd: number
address_btc: string | null
address_xmr: string | null
}
]
```
Response body (`asset` parameter specified)
```ts
[
{
title: string
fund: 'monero' | 'firo' | 'privacyguides' | 'general'
date: string // YYYY-MM-DD
author: string
url: string
is_funded: boolean
raised_amount_percent: number
contributions: number
asset: 'BTC' | 'XMR' | 'USD'
target_amount: number
remaining_amount: number
address: string | null
}
]
```
# Contributing
Pull requests welcome!
Thanks for supporting MAGIC Grants.
# License
[MIT](LICENSE)

17
auth.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import { Session } from 'next-auth'
import { DefaultJWT, JWT } from 'next-auth/jwt'
declare module 'next-auth' {
interface Session extends Session {
error?: 'RefreshAccessTokenError'
}
}
declare module 'next-auth/jwt' {
interface JWT extends DefaultJWT {
accessToken: string
accessTokenExpiresAt: number
refreshToken: string
error?: 'RefreshAccessTokenError'
}
}

17
components.json Normal file
View 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"
}
}

42
components/CustomLink.tsx Normal file
View File

@@ -0,0 +1,42 @@
/* eslint-disable jsx-a11y/anchor-has-content */
import Link from 'next/link'
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
import { cn } from '../utils/cn'
const CustomLink = ({
href,
className,
...rest
}: DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => {
const isInternalLink = href && href.startsWith('/')
const isAnchorLink = href && href.startsWith('#')
if (isInternalLink) {
// @ts-ignore
return (
<Link
href={href}
className={cn('text-primary hover:text-primary-hover', className)}
{...rest}
/>
)
}
if (isAnchorLink) {
return (
<a href={href} className={cn('text-primary hover:text-primary-hover', className)} {...rest} />
)
}
return (
<a
target="_blank"
rel="noopener noreferrer"
className={cn('text-primary hover:text-primary-hover', className)}
href={href}
{...rest}
/>
)
}
export default CustomLink

View File

@@ -1,244 +0,0 @@
import { faMonero } from '@fortawesome/free-brands-svg-icons'
import { faCreditCard } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { useEffect, useRef, useState } from 'react'
import { MAX_AMOUNT } from '../config'
import { fetchPostJSON } from '../utils/api-helpers'
import Spinner from './Spinner'
type DonationStepsProps = {
projectNamePretty: string
projectSlug: string
}
const DonationSteps: React.FC<DonationStepsProps> = ({
projectNamePretty,
projectSlug,
}) => {
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [deductible, setDeductible] = useState('no')
const [amount, setAmount] = useState('')
const [readyToPay, setReadyToPay] = useState(false)
const [btcPayLoading, setBtcpayLoading] = useState(false)
const [fiatLoading, setFiatLoading] = useState(false)
const formRef = useRef<HTMLFormElement | null>(null)
const radioHandler = (event: React.ChangeEvent<HTMLInputElement>) => {
setDeductible(event.target.value)
}
function handleFiatAmountClick(e: React.MouseEvent, value: string) {
e.preventDefault()
setAmount(value)
}
useEffect(() => {
if (amount && typeof parseInt(amount) === 'number') {
if (deductible === 'no' || (name && email)) {
setReadyToPay(true)
} else {
setReadyToPay(false)
}
} else {
setReadyToPay(false)
}
}, [deductible, amount, email, name])
async function handleBtcPay() {
const validity = formRef.current?.checkValidity()
if (!validity) {
return
}
setBtcpayLoading(true)
try {
const payload = {
amount,
project_slug: projectSlug,
project_name: projectNamePretty
}
if (email) {
Object.assign(payload, { email })
}
if (name) {
Object.assign(payload, { name })
}
console.log(payload)
const data = await fetchPostJSON('/api/btcpay', payload)
if (data.checkoutLink) {
window.location.assign(data.checkoutLink)
} else if (data.message) {
throw new Error(data.message)
} else {
console.log({ data })
throw new Error('Something went wrong with BtcPay Server checkout.')
}
} catch (e) {
console.error(e)
}
setBtcpayLoading(false)
}
async function handleFiat() {
const validity = formRef.current?.checkValidity()
if (!validity) {
return
}
setFiatLoading(true)
try {
const data = await fetchPostJSON('/api/stripe_checkout', {
amount,
project_slug: projectSlug,
project_name: projectNamePretty,
email,
name,
})
console.log({ data })
if (data.url) {
window.location.assign(data.url)
} else {
throw new Error('Something went wrong with Stripe checkout.')
}
} catch (e) {
console.error(e)
}
setFiatLoading(false)
}
return (
<form
ref={formRef}
className="flex flex-col gap-4"
onSubmit={(e) => e.preventDefault()}
>
<section className="flex flex-col gap-1">
<h3>Do you want this donation to be tax deductible (USA only)?</h3>
<div className="flex space-x-4 pb-4">
<label>
<input
type="radio"
id="no"
name="deductible"
value="no"
onChange={radioHandler}
defaultChecked={true}
/>
No
</label>
<label>
<input
type="radio"
id="yes"
value="yes"
name="deductible"
onChange={radioHandler}
/>
Yes
</label>
</div>
<h3>
Name{' '}
<span className="text-subtle">
{deductible === 'yes' ? '(required)' : '(optional)'}
</span>
</h3>
<input
type="text"
placeholder={'MAGIC Monero Fund'}
required={deductible === 'yes'}
onChange={(e) => setName(e.target.value)}
className="mb-4"
></input>
<h3>
Email{' '}
<span className="text-subtle">
{deductible === 'yes' ? '(required)' : '(optional)'}
</span>
</h3>
<input
type="email"
placeholder={`MoneroFund@MagicGrants.org`}
required={deductible === 'yes'}
onChange={(e) => setEmail(e.target.value)}
></input>
</section>
<section>
<div className="flex justify-between items-center">
<h3>How much would you like to donate?</h3>
</div>
<div className="sm:flex-row flex flex-col gap-2 py-2" role="group">
{[50, 100, 250, 500].map((value, index) => (
<button
key={index}
className="group"
onClick={(e) => handleFiatAmountClick(e, value.toString())}
>
${value}
</button>
))}
<div className="relative flex w-full">
<div className="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
{/* <FontAwesomeIcon icon={faDollarSign} className="w-5 h-5 text-black" /> */}
<span className="w-5 h-5 font-mono text-xl mb-2">{'$'}</span>
</div>
<input
required
type="number"
id="amount"
value={amount}
onChange={(e) => {
setAmount(e.target.value)
}}
className="!pl-10 w-full"
placeholder="Or enter custom amount"
/>
</div>
</div>
</section>
<div className="flex flex-wrap items-center gap-4">
<button
name="btcpay"
onClick={handleBtcPay}
className="pay"
disabled={!readyToPay || btcPayLoading}
>
{btcPayLoading ? (
<Spinner />
) : (
<FontAwesomeIcon
icon={faMonero}
className="color-me-monero h-8 w-8"
/>
)}
<span className="whitespace-nowrap">Donate with Monero or Bitcoin</span>
</button>
<button
name="stripe"
onClick={handleFiat}
className="pay"
disabled={!readyToPay || fiatLoading}
>
{fiatLoading ? (
<Spinner />
) : (
<FontAwesomeIcon
icon={faCreditCard}
className="color-me-monero h-8 w-8"
/>
)}
<span className="whitespace-nowrap">Donate with fiat</span>
</button>
</div>
</form>
)
}
export default DonationSteps

View File

@@ -0,0 +1,319 @@
import { useEffect, useRef, useState } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { faMonero } from '@fortawesome/free-brands-svg-icons'
import { faCreditCard } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { DollarSign, Info } from 'lucide-react'
import { z } from 'zod'
import Image from 'next/image'
import { MAX_AMOUNT } from '../config'
import Spinner from './Spinner'
import { trpc } from '../utils/trpc'
import { useToast } from './ui/use-toast'
import { useSession } from 'next-auth/react'
import { Button } from './ui/button'
import { RadioGroup, RadioGroupItem } from './ui/radio-group'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form'
import { Input } from './ui/input'
import { ProjectItem } from '../utils/types'
import { useFundSlug } from '../utils/use-fund-slug'
import { Alert, AlertDescription, AlertTitle } from './ui/alert'
import CustomLink from './CustomLink'
type Props = {
project: ProjectItem | undefined
close: () => void
openRegisterModal: () => void
}
const DonationFormModal: React.FC<Props> = ({ project, openRegisterModal, close }) => {
const fundSlug = useFundSlug()
const session = useSession()
const isAuthed = session.status === 'authenticated'
const schema = z
.object({
name: z.string().optional(),
email: z.string().email().optional(),
amount: z.coerce.number().min(1).max(MAX_AMOUNT),
taxDeductible: z.enum(['yes', 'no']),
})
.refine((data) => (!isAuthed && data.taxDeductible === 'yes' ? !!data.name : true), {
message: 'Name is required when the donation is tax deductible.',
path: ['name'],
})
.refine((data) => (!isAuthed && data.taxDeductible === 'yes' ? !!data.email : true), {
message: 'Email is required when the donation is tax deductible.',
path: ['email'],
})
type FormInputs = z.infer<typeof schema>
const { toast } = useToast()
const form = useForm<FormInputs>({
resolver: zodResolver(schema),
defaultValues: {
name: '',
amount: '' as unknown as number, // a trick to get trigger to work when amount is empty
taxDeductible: 'no',
},
mode: 'all',
})
const amount = form.watch('amount')
const taxDeductible = form.watch('taxDeductible')
const donateWithFiatMutation = trpc.donation.donateWithFiat.useMutation()
const donateWithCryptoMutation = trpc.donation.donateWithCrypto.useMutation()
async function handleBtcPay(data: FormInputs) {
if (!project) return
if (!fundSlug) return
try {
const result = await donateWithCryptoMutation.mutateAsync({
email: data.email || null,
name: data.name || null,
amount: data.amount,
projectSlug: project.slug,
projectName: project.title,
fundSlug,
taxDeductible: data.taxDeductible === 'yes',
})
window.location.assign(result.url)
} catch (e) {
toast({
title: 'Sorry, something went wrong.',
variant: 'destructive',
})
}
}
async function handleFiat(data: FormInputs) {
if (!project) return
if (!fundSlug) return
try {
const result = await donateWithFiatMutation.mutateAsync({
email: data.email || null,
name: data.name || null,
amount: data.amount,
projectSlug: project.slug,
projectName: project.title,
fundSlug,
taxDeductible: data.taxDeductible === 'yes',
})
if (!result.url) throw Error()
window.location.assign(result.url)
} catch (e) {
toast({
title: 'Sorry, something went wrong.',
variant: 'destructive',
})
}
}
useEffect(() => {
form.trigger('email', { shouldFocus: true })
form.trigger('name', { shouldFocus: true })
}, [taxDeductible])
if (!project) return <></>
return (
<div className="space-y-4">
<div className="py-4 flex flex-col space-y-8">
<div className="flex flex-col items-center sm:space-x-4 sm:flex-row">
<Image
alt={project.title}
src={project.coverImage}
width={200}
height={96}
objectFit="cover"
className="w-36 rounded-xl"
/>
<div className="flex flex-col justify-center">
<h2 className="text-center sm:text-left font-semibold">{project.title}</h2>
<h3 className="text-gray-500">Pledge your support</h3>
</div>
</div>
</div>
<Form {...form}>
<form className="flex flex-col gap-4">
{!isAuthed && (
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name {taxDeductible === 'no' && '(optional)'}</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email {taxDeductible === 'no' && '(optional)'}</FormLabel>
<FormControl>
<Input placeholder="johndoe@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<FormField
control={form.control}
name="amount"
render={({ field }) => (
<FormItem>
<FormLabel>Amount</FormLabel>
<FormControl>
<div className="flex flex-row gap-2 items-center flex-wrap ">
<Input
className="w-40 mr-auto"
type="number"
inputMode="numeric"
leftIcon={DollarSign}
{...field}
/>
{[50, 100, 250, 500].map((value, index) => (
<Button
key={`amount-button-${index}`}
variant="light"
size="sm"
type="button"
onClick={() =>
form.setValue('amount', value, {
shouldValidate: true,
})
}
>
${value}
</Button>
))}
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="taxDeductible"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Do you want this donation to be tax deductible? (US only)</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-row space-x-4"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="no" />
</FormControl>
<FormLabel className="font-normal">No</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="yes" />
</FormControl>
<FormLabel className="font-normal">Yes</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{amount > 500 && taxDeductible === 'yes' && (
<Alert>
<Info className="h-4 w-4 text-primary" />
<AlertTitle>Heads up!</AlertTitle>
<AlertDescription>
When donating over $500 with crypto, you MUST complete{' '}
<CustomLink target="_blank" href="https://www.irs.gov/pub/irs-pdf/f8283.pdf">
Form 8283
</CustomLink>{' '}
and send the completed form to{' '}
<CustomLink href={`mailto:info@magicgrants.org`}>info@magicgrants.org</CustomLink>{' '}
to deduct your donation.
</AlertDescription>
</Alert>
)}
<div className="flex flex-col sm:flex-row space-y-2 sm:space-x-2 sm:space-y-0">
<Button
type="button"
onClick={form.handleSubmit(handleBtcPay)}
disabled={!form.formState.isValid || form.formState.isSubmitting}
className="grow basis-0"
>
{donateWithCryptoMutation.isPending ? (
<Spinner />
) : (
<FontAwesomeIcon icon={faMonero} className="h-5 w-5" />
)}
Donate with Crypto
</Button>
<Button
type="button"
onClick={form.handleSubmit(handleFiat)}
disabled={!form.formState.isValid || form.formState.isSubmitting}
className="grow basis-0 bg-indigo-500 hover:bg-indigo-700"
>
{donateWithFiatMutation.isPending ? (
<Spinner />
) : (
<FontAwesomeIcon icon={faCreditCard} className="h-5 w-5" />
)}
Donate with Fiat
</Button>
</div>
</form>
</Form>
{!isAuthed && <div className="w-full h-px bg-border" />}
{!isAuthed && (
<div className="flex flex-col items-center">
<p>Want to support more projects from now on?</p>
<Button
type="button"
size="lg"
variant="link"
onClick={() => (openRegisterModal(), close())}
>
Create an account
</Button>
</div>
)}
</div>
)
}
export default DonationFormModal

35
components/FiroLogo.tsx Normal file
View File

@@ -0,0 +1,35 @@
import { SVGProps } from 'react'
function FiroLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
viewBox="0 0 520 520"
xmlSpace="preserve"
{...props}
>
<style type="text/css">{'\n\t.st0{fill:#FEFEFE;}\n\t.st1{fill:#9B1C2E;}\n'}</style>
<g>
<g>
<circle className="st0" cx={260} cy={260} r={252.7} />
</g>
<g>
<path
className="st1"
d="M155.6,370.7c5.9,0,11.2-3.2,14-8.4l37.3-70.6h-57.5c-8.7,0-15.8-7.1-15.8-15.8v-31.6 c0-8.7,7.1-15.8,15.8-15.8h90.9l70.6-133.9c2.7-5.2,8.1-8.4,14-8.4h118.8C397.5,37.4,332.3,7,260,7C120.3,7,7,120.3,7,260 c0,39.7,9.2,77.3,25.5,110.7H155.6z"
/>
<path
className="st1"
d="M364.4,149.3c-5.9,0-11.2,3.2-14,8.4l-37.3,70.6h57.5c8.7,0,15.8,7.1,15.8,15.8v31.6 c0,8.7-7.1,15.8-15.8,15.8h-90.9l-70.6,133.9c-2.7,5.2-8.1,8.4-14,8.4H76.4C122.5,482.6,187.7,513,260,513 c139.7,0,253-113.3,253-253c0-39.7-9.2-77.3-25.5-110.7H364.4z"
/>
</g>
</g>
</svg>
)
}
export default FiroLogo

33
components/Footer.tsx Normal file
View File

@@ -0,0 +1,33 @@
import CustomLink from './CustomLink'
// import SocialIcon from '@/components/social-icons'
function Footer() {
return (
<footer className="pb-4 mt-16 flex flex-col items-center space-y-2">
<div className="flex flex-row space-x-2 justify-center">
<CustomLink href="/terms" className="text-xs hover:underline">
Terms of Use
</CustomLink>
<span className="text-xs text-muted-foreground">|</span>
<CustomLink href="/privacy" className="text-xs hover:underline">
Privacy Policy
</CustomLink>
</div>
<div className="space-x-4 text-center text-xs text-gray-500">
MAGIC Grants is a 501(c)(3) non-profit organization. All gifts and donations are
tax-deductible to the full extent of the law.
</div>
<div className="space-x-4 text-center text-xs text-gray-500">
© {new Date().getFullYear()} MAGIC Grants. This website builds upon technology by Open
Sats.
</div>
</footer>
)
}
export default Footer

View File

@@ -1,29 +1,163 @@
import Nav from 'react-bootstrap/Nav'
import Navbar from 'react-bootstrap/Navbar'
import Container from 'react-bootstrap/Container'
// import NavDropdown from 'react-bootstrap/NavDropdown'
// import Row from 'react-bootstrap/Row'
// import Col from 'react-bootstrap/Col'
// import Image from 'next/image'
// import Link from 'next/link'
// import samplelogo from '/public/favicon.png'
import { useEffect, useState } from 'react'
import { signOut, useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import Link from './CustomLink'
import MobileNav from './MobileNav'
import { fundHeaderNavLinks } from '../data/headerNavLinks'
import MagicLogo from './MagicLogo'
import { Dialog, DialogContent, DialogTrigger } from './ui/dialog'
import { Button } from './ui/button'
import RegisterFormModal from './RegisterFormModal'
import LoginFormModal from './LoginFormModal'
import PasswordResetFormModal from './PasswordResetFormModal'
import { Avatar, AvatarFallback } from './ui/avatar'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from './ui/dropdown-menu'
import { useFundSlug } from '../utils/use-fund-slug'
import CustomLink from './CustomLink'
import { funds } from '../utils/funds'
import MoneroLogo from './MoneroLogo'
import FiroLogo from './FiroLogo'
import PrivacyGuidesLogo from './PrivacyGuidesLogo'
const Header = () => {
const [registerIsOpen, setRegisterIsOpen] = useState(false)
const [loginIsOpen, setLoginIsOpen] = useState(false)
const [passwordResetIsOpen, setPasswordResetIsOpen] = useState(false)
const router = useRouter()
const session = useSession()
const fundSlug = useFundSlug()
useEffect(() => {
if (router.query.loginEmail) {
setLoginIsOpen(true)
}
}, [router.query.loginEmail])
const fund = fundSlug ? funds[fundSlug] : null
return (
<header className="pb-10 sm:pb-10">
<Navbar collapseOnSelect expand="lg" variant="light" className="color-nav navbar-expand-sm px-2 sm:px-4 py-2.5 fixed w-full z-20 top-0 left-0">
<Container>
<Navbar.Brand href="/">MAGIC Monero Fund</Navbar.Brand>
<Navbar.Toggle aria-controls="responsive-navbar-nav" />
<Navbar.Collapse id="responsive-navbar-nav">
<Nav className="me-auto">
<Nav.Link href="/apply">Apply</Nav.Link>
<Nav.Link href="/faq">FAQs</Nav.Link>
<Nav.Link href="/about">About Us</Nav.Link>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
<header className="flex items-center justify-between py-10">
<div>
<Link
href={fundSlug ? `/${fundSlug}` : '/'}
aria-label="Home"
className="flex items-center mr-3 gap-4"
>
{!fundSlug && <MagicLogo className="w-12 h-12" />}
{fundSlug === 'monero' && <MoneroLogo className="w-12 h-12" />}
{fundSlug === 'firo' && <FiroLogo className="w-12 h-12" />}
{fundSlug === 'privacyguides' && <PrivacyGuidesLogo className="w-12 h-12" />}
{fundSlug === 'general' && <MagicLogo className="w-12 h-12" />}
<span className="text-foreground text-lg font-bold hidden sm:block">
{fund ? fund.title : 'MAGIC Grants'}
</span>
</Link>
</div>
<div className="flex gap-2 items-center text-base leading-5">
{!!fund &&
fundHeaderNavLinks[fund.slug].map((link) => (
<CustomLink
key={link.title}
href={`/${fundSlug}/${link.href}`}
className={
link.isButton
? 'rounded border border-primary bg-transparent px-4 py-2 font-semibold text-primary hover:border-transparent hover:bg-primary hover:text-white'
: 'hidden p-1 font-medium text-gray-900 sm:p-4 md:inline-block'
}
>
{link.title}
</CustomLink>
))}
{!!fund && session.status !== 'authenticated' && (
<>
<Dialog open={loginIsOpen} onOpenChange={setLoginIsOpen}>
<DialogTrigger asChild>
<Button variant="outline" className="w-18 block sm:hidden" size="sm">
Login
</Button>
</DialogTrigger>
<DialogTrigger asChild>
<Button variant="outline" className="w-24 hidden sm:block">
Login
</Button>
</DialogTrigger>
<DialogContent>
<LoginFormModal
close={() => setLoginIsOpen(false)}
openRegisterModal={() => setRegisterIsOpen(true)}
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
/>
</DialogContent>
</Dialog>
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
<DialogTrigger asChild>
<Button className="w-18 block sm:hidden" size="sm">
Register
</Button>
</DialogTrigger>
<DialogTrigger asChild>
<Button className="w-24 hidden sm:block">Register</Button>
</DialogTrigger>
<DialogContent>
<RegisterFormModal
close={() => setRegisterIsOpen(false)}
openLoginModal={() => setLoginIsOpen(true)}
/>
</DialogContent>
</Dialog>
</>
)}
{/* <ThemeSwitch /> */}
{!!fund && session.status === 'authenticated' && (
<DropdownMenu>
<DropdownMenuTrigger>
<Avatar>
<AvatarFallback>
{session.data.user?.email?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<CustomLink href={`/${fundSlug}/account/my-donations`} className="text-foreground">
<DropdownMenuItem>My Donations</DropdownMenuItem>
</CustomLink>
<CustomLink href={`/${fundSlug}/account/my-memberships`} className="text-foreground">
<DropdownMenuItem>My Memberships</DropdownMenuItem>
</CustomLink>
<CustomLink href={`/${fundSlug}/account/settings`} className="text-foreground">
<DropdownMenuItem>Settings</DropdownMenuItem>
</CustomLink>
<DropdownMenuItem onClick={() => signOut({ callbackUrl: `/${fundSlug}` })}>
Logout
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{!!fundSlug && <MobileNav />}
</div>
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
<DialogContent>
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
</DialogContent>
</Dialog>
</header>
)
}

View File

@@ -1,41 +1,49 @@
import Navbar from './Header'
import React from 'react'
import Head from 'next/head'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faBug } from '@fortawesome/free-solid-svg-icons'
import Link from 'next/link'
import { ReactNode, useEffect } from 'react'
import { signOut, useSession } from 'next-auth/react'
import { Inter } from 'next/font/google'
import SectionContainer from './SectionContainer'
import Footer from './Footer'
import Header from './Header'
import { useFundSlug } from '../utils/use-fund-slug'
interface Props {
children: ReactNode
}
const inter = Inter({ subsets: ['latin'] })
const LayoutWrapper = ({ children }: Props) => {
const fundSlug = useFundSlug()
const { data: session } = useSession()
useEffect(() => {
if (session?.error === 'RefreshAccessTokenError') {
if (fundSlug) {
signOut({ callbackUrl: `/${fundSlug}/?loginEmail=${session?.user.email}` })
} else {
signOut({ callbackUrl: '/' })
}
}
}, [session])
const Layout: React.FC<React.PropsWithChildren> = ({ children }) => {
return (
<div className="min-h-screen flex flex-col">
<Head>
<title>MAGIC Monero Fund</title>
<meta name="description" content="TKTK" />
<link rel="icon" href="https://monerofund.org/favicon.ico" />
<>
<style jsx global>{`
body {
font-family: ${inter.style.fontFamily};
}
`}</style>
{/* Twitter */}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:creator" content="@MagicGrants" />
<meta name="twitter:site" content="@MagicGrants" />
<meta name="twitter:title" content="MAGIC Monero Fund" />
<meta name="twitter:image" content="https://monerofund.org/img/crystalball.jpg" />
{/* Open Graph */}
<meta property="og:url" content="https://monerofund.org/" key="ogurl" />
<meta property="og:image" content="https://monerofund.org/img/crystalball.jpg" key="ogimage" />
<meta property="og:site_name" content="MAGIC Monero Fund" key="ogsitename" />
<meta property="og:title" content="MAGIC Monero Fund" key="ogtitle" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:description" content="Support the MAGIC Monero Fund and open source software and research for the Monero Project." key="ogdesc" />
</Head>
<Navbar />
<main className="flex-1 flex flex-col">{ children }</main>
<footer className="flex justify-between p-4 md:p-8 bg-light">
<strong>© MAGIC Grants. This website builds upon technology by Open Sats.</strong>
</footer>
</div>
<SectionContainer>
<div className="flex h-screen flex-col justify-between">
<Header />
<main className="grow">{children}</main>
<Footer />
</div>
</SectionContainer>
</>
)
}
export default Layout
export default LayoutWrapper

View File

@@ -0,0 +1,149 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { zodResolver } from '@hookform/resolvers/zod'
import { signIn, useSession } from 'next-auth/react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
import { Input } from './ui/input'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form'
import { Button } from './ui/button'
import { useToast } from './ui/use-toast'
import { useFundSlug } from '../utils/use-fund-slug'
import Spinner from './Spinner'
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
type LoginFormInputs = z.infer<typeof schema>
type Props = {
close: () => void
openPasswordResetModal: () => void
openRegisterModal: () => void
}
function LoginFormModal({ close, openPasswordResetModal, openRegisterModal }: Props) {
const { toast } = useToast()
const router = useRouter()
const fundSlug = useFundSlug()
const form = useForm<LoginFormInputs>({
resolver: zodResolver(schema),
defaultValues: { email: '', password: '' },
shouldFocusError: false,
})
useEffect(() => {
if (!fundSlug) return
if (router.query.loginEmail) {
form.setValue('email', router.query.loginEmail as string)
setTimeout(() => form.setFocus('password'), 100)
router.replace(`/${fundSlug}`)
}
}, [router.query.loginEmail])
async function onSubmit(data: LoginFormInputs) {
const result = await signIn('credentials', {
redirect: false,
email: data.email,
password: data.password,
})
if (result?.error) {
if (result.error === 'INVALID_CREDENTIALS') {
return form.setError(
'password',
{ message: 'Invalid email or password.' },
{ shouldFocus: true }
)
}
return toast({
title: 'Sorry, something went wrong.',
variant: 'destructive',
})
}
toast({
title: 'Successfully logged in!',
})
close()
}
return (
<>
<DialogHeader>
<DialogTitle>Login</DialogTitle>
<DialogDescription>Log into your account.</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>
)}
/>
<div className="flex flex-col">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="button"
onClick={() => (openPasswordResetModal(), close())}
variant="link"
className="self-end"
>
I forgot my password
</Button>
</div>
<div className="flex flex-row space-x-2">
<Button
className="grow basis-0"
variant="outline"
type="button"
onClick={() => (openRegisterModal(), close())}
>
Register
</Button>
<Button
className="grow basis-0"
type="submit"
disabled={!form.formState.isValid || form.formState.isSubmitting}
>
{form.formState.isSubmitting && <Spinner />} Login
</Button>
</div>
</form>
</Form>
</>
)
}
export default LoginFormModal

725
components/LogoCrystal.tsx Normal file

File diff suppressed because one or more lines are too long

118
components/MagicLogo.tsx Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,295 @@
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { faMonero } from '@fortawesome/free-brands-svg-icons'
import { faCreditCard } from '@fortawesome/free-solid-svg-icons'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { DollarSign } from 'lucide-react'
import { useSession } from 'next-auth/react'
import { z } from 'zod'
import Image from 'next/image'
import { MAX_AMOUNT } from '../config'
import Spinner from './Spinner'
import { trpc } from '../utils/trpc'
import { useToast } from './ui/use-toast'
import { Button } from './ui/button'
import { RadioGroup, RadioGroupItem } from './ui/radio-group'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form'
import { Input } from './ui/input'
import { ProjectItem } from '../utils/types'
import { useFundSlug } from '../utils/use-fund-slug'
type Props = {
project: ProjectItem | undefined
close: () => void
openRegisterModal: () => void
}
const MembershipFormModal: React.FC<Props> = ({ project, close, openRegisterModal }) => {
const fundSlug = useFundSlug()
const session = useSession()
const isAuthed = session.status === 'authenticated'
const schema = z
.object({
name: z.string().optional(),
email: z.string().email().optional(),
amount: z.coerce.number().min(1).max(MAX_AMOUNT),
taxDeductible: z.enum(['yes', 'no']),
recurring: z.enum(['yes', 'no']),
})
.refine((data) => (!isAuthed && data.taxDeductible === 'yes' ? !!data.name : true), {
message: 'Name is required when the donation is tax deductible.',
path: ['name'],
})
.refine((data) => (!isAuthed && data.taxDeductible === 'yes' ? !!data.email : true), {
message: 'Email is required when the donation is tax deductible.',
path: ['email'],
})
type FormInputs = z.infer<typeof schema>
const { toast } = useToast()
const form = useForm<FormInputs>({
resolver: zodResolver(schema),
defaultValues: {
name: '',
amount: 100, // a trick to get trigger to work when amount is empty
taxDeductible: 'no',
recurring: 'no',
},
mode: 'all',
})
const taxDeductible = form.watch('taxDeductible')
const recurring = form.watch('recurring')
const payMembershipWithFiatMutation = trpc.donation.payMembershipWithFiat.useMutation()
const payMembershipWithCryptoMutation = trpc.donation.payMembershipWithCrypto.useMutation()
async function handleBtcPay(data: FormInputs) {
if (!project) return
if (!fundSlug) return
try {
const result = await payMembershipWithCryptoMutation.mutateAsync({
projectSlug: project.slug,
projectName: project.title,
fundSlug,
taxDeductible: data.taxDeductible === 'yes',
})
window.location.assign(result.url)
} catch (e) {
toast({
title: 'Sorry, something went wrong.',
variant: 'destructive',
})
}
}
async function handleFiat(data: FormInputs) {
if (!project) return
if (!fundSlug) return
try {
const result = await payMembershipWithFiatMutation.mutateAsync({
projectSlug: project.slug,
projectName: project.title,
fundSlug,
recurring: data.recurring === 'yes',
taxDeductible: data.taxDeductible === 'yes',
})
if (!result.url) throw Error()
window.location.assign(result.url)
} catch (e) {
toast({
title: 'Sorry, something went wrong.',
variant: 'destructive',
})
}
}
if (!project) return <></>
return (
<div className="space-y-4">
<div className="py-4 flex flex-col space-y-8">
<div className="flex flex-col items-center sm:space-x-4 sm:flex-row">
<Image
alt={project.title}
src={project.coverImage}
width={200}
height={96}
objectFit="cover"
className="w-36 rounded-xl"
/>
<div className="flex flex-col justify-center">
<h2 className="text-center sm:text-left font-semibold">{project.title}</h2>
<h3 className="text-gray-500">Pledge your support</h3>
</div>
</div>
</div>
<Form {...form}>
<form className="flex flex-col gap-4">
{!isAuthed && (
<>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name {taxDeductible === 'no' && '(optional)'}</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email {taxDeductible === 'no' && '(optional)'}</FormLabel>
<FormControl>
<Input placeholder="johndoe@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
)}
<div className="flex flex-col space-y-3">
<FormLabel>Amount</FormLabel>
<span className="flex flex-row">
<DollarSign className="text-primary" />
100.00
</span>
</div>
<FormField
control={form.control}
name="taxDeductible"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Do you want your membership to be tax deductible? (US only)</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-row space-x-4"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="no" />
</FormControl>
<FormLabel className="font-normal">No</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="yes" />
</FormControl>
<FormLabel className="font-normal">Yes</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="recurring"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>
Do you want your membership payment to be recurring? (Fiat only)
</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-row space-x-4"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="no" />
</FormControl>
<FormLabel className="font-normal">No</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="yes" />
</FormControl>
<FormLabel className="font-normal">Yes</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="mt-4 flex flex-col sm:flex-row space-y-2 sm:space-x-2 sm:space-y-0">
<Button
type="button"
onClick={form.handleSubmit(handleBtcPay)}
disabled={!form.formState.isValid || form.formState.isSubmitting}
className="grow basis-0"
>
{payMembershipWithCryptoMutation.isPending ? (
<Spinner />
) : (
<FontAwesomeIcon icon={faMonero} className="h-5 w-5" />
)}
Pay with Crypto
</Button>
<Button
type="button"
onClick={form.handleSubmit(handleFiat)}
disabled={!form.formState.isValid || form.formState.isSubmitting}
className="grow basis-0 bg-indigo-500 hover:bg-indigo-700"
>
{payMembershipWithFiatMutation.isPending ? (
<Spinner />
) : (
<FontAwesomeIcon icon={faCreditCard} className="h-5 w-5" />
)}
Pay with Fiat
</Button>
</div>
</form>
</Form>
{!isAuthed && <div className="w-full h-px bg-border" />}
{!isAuthed && (
<div className="flex flex-col items-center ">
<p>Want to support more projects from now on?</p>
<Button
type="button"
size="lg"
variant="link"
onClick={() => (openRegisterModal(), close())}
>
Create an account
</Button>
</div>
)}
</div>
)
}
export default MembershipFormModal

90
components/MobileNav.tsx Normal file
View File

@@ -0,0 +1,90 @@
import { useState } from 'react'
import CustomLink from './CustomLink'
import { fundHeaderNavLinks } from '../data/headerNavLinks'
import { useFundSlug } from '../utils/use-fund-slug'
import { funds } from '../utils/funds'
const MobileNav = () => {
const [navShow, setNavShow] = useState(false)
const fundSlug = useFundSlug()
const fund = fundSlug ? funds[fundSlug] : null
const onToggleNav = () => {
setNavShow((status) => {
if (status) {
document.body.style.overflow = 'auto'
} else {
// Prevent scrolling
document.body.style.overflow = 'hidden'
}
return !status
})
}
return (
<div className="md:hidden">
<button
className="ml-1 mr-1 h-8 w-8 rounded py-1"
aria-label="Toggle Menu"
onClick={onToggleNav}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-8 w-8 text-gray-900"
>
<path
fillRule="evenodd"
d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
clipRule="evenodd"
/>
</svg>
</button>
<div
className={`fixed left-0 top-0 z-10 h-full w-full transform bg-background opacity-95 duration-300 ease-in-out ${
navShow ? 'translate-x-0' : 'translate-x-full'
}`}
>
<div className="flex justify-end">
<button
className="mr-5 mt-11 h-8 w-8 rounded"
aria-label="Toggle Menu"
onClick={onToggleNav}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-8 w-8 text-gray-900"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
<nav className="fixed mt-8 h-full">
{!!fund &&
fundHeaderNavLinks[fund.slug].map((link) => (
<div key={link.title} className="px-12 py-4">
<CustomLink
href={`/${fundSlug}/${link.href}`}
className="text-2xl font-bold tracking-tight text-gray-900"
onClick={onToggleNav}
>
{link.title}
</CustomLink>
</div>
))}
</nav>
</div>
</div>
)
}
export default MobileNav

58
components/MoneroLogo.tsx Normal file
View File

@@ -0,0 +1,58 @@
import { SVGProps } from 'react'
function MoneroLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
width="85.086304mm"
height="85.084808mm"
viewBox="0 0 85.086304 85.084808"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<defs id="defs1">
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath2">
<path
d="M 0,841.889 H 595.275 V 0 H 0 Z"
transform="translate(-193.3003,-554.52051)"
id="path2"
/>
</clipPath>
<clipPath clipPathUnits="userSpaceOnUse" id="clipPath4">
<path
d="M 0,841.889 H 595.275 V 0 H 0 Z"
transform="translate(-187.7847,-507.50881)"
id="path4"
/>
</clipPath>
</defs>
<g id="layer1">
<path
id="path1"
d="m 0,0 c -20.377,0 -36.903,-16.524 -36.903,-36.902 0,-4.074 0.66,-7.992 1.88,-11.657 h 11.036 V -17.51 L 0,-41.497 23.987,-17.51 v -31.049 h 11.037 c 1.22,3.665 1.88,7.583 1.88,11.657 C 36.904,-16.524 20.378,0 0,0"
style={{
fill: '#ff6600',
fillOpacity: 1,
fillRule: 'nonzero',
stroke: 'none',
}}
transform="matrix(1.1528216,0,0,-1.1528216,42.542576,0)"
clipPath="url(#clipPath2)"
/>
<path
id="path3"
d="M 0,0 -10.468,10.469 V -9.068 h -4.002 -4.002 -7.546 c 6.478,-10.628 18.178,-17.726 31.533,-17.726 13.355,0 25.056,7.098 31.533,17.726 H 29.501 22.344 21.499 V 10.469 L 11.03,0 5.515,-5.515 Z"
style={{
fill: '#4c4c4c',
fillOpacity: 1,
fillRule: 'nonzero',
stroke: 'none',
}}
transform="matrix(1.1528216,0,0,-1.1528216,36.184075,54.196107)"
clipPath="url(#clipPath4)"
/>
</g>
</svg>
)
}
export default MoneroLogo

View File

@@ -0,0 +1,77 @@
import { networkFor, SocialIcon } from 'react-social-icons'
import { ReactNode } from 'react'
import Image from 'next/image'
import { ProjectItem } from '../utils/types'
import CustomLink from './CustomLink'
import WebIcon from './WebIcon'
interface Props {
project: ProjectItem
children: ReactNode
}
export default function PageHeading({ project, children }: Props) {
return (
<div className="divide-y divide-gray-200">
<div className="items-start space-y-2 pb-8 pt-6 md:space-y-5 xl:grid xl:grid-cols-3 xl:gap-x-8">
<Image
src={project.coverImage}
alt="avatar"
width={300}
height={300}
className="h-60 w-60 mx-auto my-auto object-contain row-span-3 hidden xl:block"
/>
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14 xl:col-span-2">
{!!project.website && (
<CustomLink
className="text-inherit hover:text-inherit hover:underline"
href={project.website}
>
{project.title}
</CustomLink>
)}
{!project.website && project.title}
</h1>
<p>{project.summary}</p>
<div></div>
<div className="flex space-x-3 items-center">
<p>
by <CustomLink href={project.socialLinks[0]}>{project.nym}</CustomLink>
</p>
<div className="flex">
{project.socialLinks.map((link) =>
networkFor(link) !== 'sharethis' ? (
<SocialIcon
key={`social-icon-${link}`}
url={link}
className="text-gray-700 hover:text-primary transition-colors"
style={{ width: 40, height: 40 }}
fgColor="currentColor"
bgColor="transparent"
/>
) : (
<CustomLink
key={`social-icon-${link}`}
href={link}
className="text-gray-700 hover:text-primary"
>
<WebIcon style={{ width: 40, height: 40, padding: 8 }} />
</CustomLink>
)
)}
</div>
</div>
</div>
<div className="items-start space-y-2 xl:grid xl:grid-cols-3 xl:gap-x-8 xl:space-y-0">
{children}
</div>
</div>
)
}

View File

@@ -0,0 +1,71 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
import { Input } from './ui/input'
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from './ui/form'
import { Button } from './ui/button'
import { useToast } from './ui/use-toast'
import { trpc } from '../utils/trpc'
import Spinner from './Spinner'
const schema = z.object({
email: z.string().email(),
})
type PasswordResetFormInputs = z.infer<typeof schema>
type Props = { close: () => void }
function PasswordResetFormModal({ close }: Props) {
const { toast } = useToast()
const form = useForm<PasswordResetFormInputs>({ resolver: zodResolver(schema) })
const requestPasswordResetMutation = trpc.auth.requestPasswordReset.useMutation()
async function onSubmit(data: PasswordResetFormInputs) {
try {
await requestPasswordResetMutation.mutateAsync(data)
toast({ title: 'A password reset link has been sent to your email.' })
close()
} catch (error) {
toast({ title: 'Sorry, something went wrong.', variant: 'destructive' })
}
}
return (
<>
<DialogHeader>
<DialogTitle>Reset Password</DialogTitle>
<DialogDescription>Recover your account.</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>
)}
/>
<Button type="submit" disabled={!form.formState.isValid || form.formState.isSubmitting}>
{form.formState.isSubmitting && <Spinner />} Reset Password
</Button>
</form>
</Form>
</>
)
}
export default PasswordResetFormModal

View File

@@ -1,67 +0,0 @@
import ReactModal from 'react-modal'
import Image from 'next/image'
import waffledog from '../public/waffledog.jpg'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faClose } from '@fortawesome/free-solid-svg-icons'
import DonationForm from './DonationForm'
import { ProjectItem } from '../utils/types'
type ModalProps = {
isOpen: boolean
onRequestClose: () => void
project: ProjectItem | undefined
}
const PaymentModal: React.FC<ModalProps> = ({
isOpen,
onRequestClose,
project,
}) => {
if (!project) {
// We never see this yeah?
return <div />
}
return (
<ReactModal
isOpen={isOpen}
onRequestClose={onRequestClose}
className="p-16 sm:p-8 bg-white shadow-xl overflow-y-auto max-h-full sm:rounded-xl w-full sm:m-8"
overlayClassName="inset-0 fixed bg-[rgba(0,_0,_0,_0.75)] flex items-center justify-center"
appElement={
typeof window === 'undefined'
? undefined
: document?.getElementById('root') || undefined
}
>
<div className="flex justify-end relative -mb-12">
<FontAwesomeIcon
icon={faClose}
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
onClick={onRequestClose}
/>
</div>
<div className="flex flex-col space-y-4 py-4">
<div className="flex gap-4 items-center">
<Image
alt={project.title}
src={project.coverImage}
width={96}
height={96}
objectFit="cover"
className="rounded-xl"
/>
<div className="flex flex-col">
<h2 className="font-sans font-bold">{project.title}</h2>
<h3 className="font-sans text-textgray">Pledge your support</h3>
</div>
</div>
</div>
<DonationForm
projectNamePretty={project.title}
projectSlug={project.slug}
/>
</ReactModal>
)
}
export default PaymentModal

View File

@@ -0,0 +1,36 @@
import { SVGProps } from 'react'
function PrivacyGuidesLogo(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlnsXlink="http://www.w3.org/1999/xlink"
width="100%"
height="100%"
viewBox="0 0 33 34"
xmlSpace="preserve"
style={{
fillRule: 'evenodd',
clipRule: 'evenodd',
strokeLinejoin: 'round',
strokeMiterlimit: 2,
}}
{...props}
>
<path
d="M4.581,4.337c-0.113,0.379 -0.049,0.822 0.077,1.707l1.604,11.224c0.277,1.939 0.415,2.909 0.782,3.775c0.325,0.768 0.781,1.474 1.346,2.087c0.638,0.691 1.465,1.217 3.117,2.269l2.349,1.495c1.126,0.716 1.69,1.075 2.295,1.214c0.465,0.108 0.947,0.121 1.416,0.042c-0.388,-0.887 -0.603,-1.867 -0.603,-2.897c0,-3.996 3.24,-7.236 7.236,-7.236c1.166,0 2.268,0.276 3.243,0.766c0.069,-0.432 0.14,-0.929 0.223,-1.514l0,-0.001l1.604,-11.224c0.126,-0.885 0.19,-1.328 0.077,-1.707c-0.099,-0.334 -0.292,-0.632 -0.557,-0.859c-0.3,-0.257 -0.73,-0.38 -1.59,-0.626l-9.441,-2.697c-0.296,-0.085 -0.444,-0.127 -0.594,-0.144c-0.134,-0.015 -0.268,-0.015 -0.402,0c-0.15,0.017 -0.298,0.059 -0.594,0.144l-9.441,2.697c-0.86,0.246 -1.29,0.369 -1.59,0.626c-0.265,0.227 -0.458,0.525 -0.557,0.859Z"
style={{
fill: '#ffd06f',
}}
/>
<path
d="M13.246,2.719c0.066,-0.007 0.134,-0.007 0.201,0c0.057,0.007 0.122,0.022 0.446,0.114l9.44,2.698c0.444,0.126 0.727,0.208 0.94,0.287c0.202,0.075 0.274,0.124 0.311,0.156c0.132,0.113 0.229,0.262 0.278,0.429c0.014,0.047 0.03,0.133 0.016,0.348c-0.015,0.226 -0.056,0.518 -0.122,0.974l-1.346,9.426c-4.125,0.397 -7.351,3.873 -7.351,8.102c0,0.835 0.126,1.641 0.36,2.4l-0.451,0.286c-1.183,0.753 -1.594,1.001 -2.012,1.097c-0.401,0.092 -0.818,0.092 -1.22,0c-0.417,-0.096 -0.829,-0.344 -2.012,-1.097l-2.349,-1.494c-1.693,-1.078 -2.398,-1.535 -2.938,-2.12c-0.495,-0.536 -0.894,-1.153 -1.178,-1.825c-0.31,-0.733 -0.436,-1.564 -0.72,-3.551l-1.603,-11.224c-0.066,-0.456 -0.107,-0.748 -0.121,-0.974c-0.015,-0.215 0.001,-0.301 0.015,-0.348c0.05,-0.167 0.146,-0.316 0.279,-0.429c0.036,-0.032 0.109,-0.081 0.31,-0.156c0.213,-0.079 0.496,-0.161 0.94,-0.287l9.44,-2.698c0.324,-0.092 0.389,-0.107 0.447,-0.114Zm13.306,5.231l-1.318,9.228c4.007,0.508 7.106,3.93 7.106,8.075c0,4.496 -3.644,8.141 -8.14,8.141c-3.01,0 -5.639,-1.634 -7.048,-4.064l-0.212,0.136l-0.135,0.085c-0.996,0.634 -1.683,1.072 -2.443,1.248c-0.668,0.154 -1.364,0.154 -2.032,0c-0.76,-0.176 -1.447,-0.614 -2.443,-1.248l-0.134,-0.085l-2.466,-1.57l0,0c-1.541,-0.98 -2.461,-1.565 -3.179,-2.344c-0.637,-0.689 -1.149,-1.483 -1.515,-2.347c-0.413,-0.976 -0.567,-2.054 -0.825,-3.863l-1.628,-11.392c-0.059,-0.416 -0.111,-0.778 -0.131,-1.081c-0.021,-0.323 -0.012,-0.648 0.087,-0.98c0.148,-0.501 0.439,-0.949 0.835,-1.289c0.264,-0.226 0.557,-0.366 0.86,-0.478c0.285,-0.106 0.636,-0.206 1.04,-0.322l0.031,-0.009l9.44,-2.697l0.05,-0.014c0.247,-0.071 0.465,-0.133 0.693,-0.159c0.2,-0.022 0.402,-0.022 0.603,0c0.227,0.026 0.445,0.088 0.692,0.159l0.05,0.014l9.471,2.706c0.404,0.116 0.755,0.216 1.04,0.322c0.304,0.112 0.596,0.252 0.86,0.478c0.397,0.34 0.687,0.788 0.835,1.289c0.099,0.332 0.108,0.657 0.087,0.98c-0.02,0.303 -0.072,0.665 -0.131,1.08l0,0.001Zm-2.352,10.972c-3.497,0 -6.332,2.835 -6.332,6.331c0,3.497 2.835,6.332 6.332,6.332c3.497,0 6.331,-2.835 6.331,-6.332c0,-3.496 -2.834,-6.331 -6.331,-6.331Zm4.313,4.197c0.319,-0.384 0.268,-0.954 -0.116,-1.274c-0.384,-0.32 -0.954,-0.268 -1.274,0.116l-3.888,4.666l-2.013,-2.013c-0.354,-0.353 -0.926,-0.353 -1.28,0c-0.353,0.353 -0.353,0.926 0,1.279l2.714,2.713c0.18,0.18 0.427,0.276 0.68,0.264c0.254,-0.011 0.492,-0.129 0.654,-0.324l4.523,-5.427Zm-19.689,-10.529c0,-2.497 2.024,-4.522 4.522,-4.522c2.498,0 4.522,2.025 4.522,4.522c0,1.48 -0.71,2.794 -1.809,3.619l0,3.617c0,1.499 -1.214,2.714 -2.713,2.714c-1.499,0 -2.713,-1.215 -2.713,-2.714l0,-3.617c-1.099,-0.825 -1.809,-2.139 -1.809,-3.619Zm5.426,4.523l-1.808,0l0,2.713c0,0.5 0.405,0.905 0.904,0.905c0.5,0 0.904,-0.405 0.904,-0.905l0,-2.713Zm-0.904,-1.809c1.499,0 2.713,-1.215 2.713,-2.714c0,-1.498 -1.214,-2.713 -2.713,-2.713c-1.499,0 -2.713,1.215 -2.713,2.713c0,1.499 1.214,2.714 2.713,2.714Z"
style={{
fill: '#28323f',
}}
/>
</svg>
)
}
export default PrivacyGuidesLogo

View File

@@ -1,12 +1,24 @@
import ProgressBar from 'react-bootstrap/ProgressBar';
type ProgressProps = { current: number; goal: number }
type ProgressProps = {
text: number;
};
const Progress = ({ current, goal }: ProgressProps) => {
const percent = Math.floor((current / goal) * 100)
const AnimatedExample = (props: ProgressProps) => {
const { text } = props;
return <ProgressBar animated variant="success" now={text} label={`${text}%`} />;
return (
<div className="w-full flex flex-col items-center space-y-1">
{/* <div className="w-full flex flex-row justify-between font-semibold">
<span>0</span> <span>${numberFormat.format(goal)}</span>
</div> */}
<div className="w-full bg-primary/15 rounded-full h-4">
<div
className="bg-green-500 h-4 rounded-full text-xs"
style={{ width: `${percent < 100 ? percent : 100}%` }}
/>
</div>
<span className="text-sm font-semibold">{percent < 100 ? percent : 100}%</span>
</div>
)
}
export default AnimatedExample;
export default Progress

View File

@@ -1,72 +1,86 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faArrowRight } from '@fortawesome/free-solid-svg-icons'
import escapeHTML from 'escape-html'
import Image from 'next/image'
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { ProjectItem } from '../utils/types'
import ShareButtons from './ShareButtons'
import { useFundSlug } from '../utils/use-fund-slug'
import Progress from './Progress'
import { cn } from '../utils/cn'
const numberFormat = Intl.NumberFormat('en', { notation: 'compact', compactDisplay: 'short' })
export type ProjectCardProps = {
project: ProjectItem
openPaymentModal: (project: ProjectItem) => void
customImageStyles?: React.CSSProperties
}
const ProjectCard: React.FC<ProjectCardProps> = ({
project,
openPaymentModal,
}) => {
const { slug, title, summary, coverImage, git, twitter, personalTwitter, personalWebsite, nym, goal, isFunded } =
project
const ProjectCard: React.FC<ProjectCardProps> = ({ project, customImageStyles }) => {
const [isHorizontal, setIsHorizontal] = useState<boolean | null>(null)
useEffect(() => {
const img = document.createElement('img')
img.src = project.coverImage
// check if image is horizontal - added additional 10% to height to ensure only true
// horizontals get flagged.
img.onload = () => {
const { naturalWidth, naturalHeight } = img
const isHorizontal = naturalWidth >= naturalHeight * 1.1
setIsHorizontal(isHorizontal)
}
}, [project.coverImage])
return (
<figure className=" bg-white space-y-4 border border-lightgray rounded-xl h-full">
<div className="relative h-64 w-full">
<Link href={`/projects/${escapeHTML(slug)}`} passHref>
<div className='relative h-64 w-full'>
<Image
alt={title}
src={coverImage}
layout="fill"
objectFit="cover"
objectPosition="50% 50%"
className="rounded-t-xl border border-lightgray cursor-pointer"
/>
</div>
</Link>
</div>
<figcaption className="p-4 space-y-4">
<h2>{title}</h2>
<p>
by{' '}
<Link href={escapeHTML(personalWebsite)} passHref legacyBehavior>
<a className="projectlist">{nym}</a>
</Link>
</p>
<p className="prose line-clamp-3">{summary}</p>
<div className="flex justify-end"></div>
<ShareButtons project={project} />
<div className="flex space-x-4 items-center justify-center pt-4">
{(isFunded)? `` : <button
className="bg-black basis-1/2"
onClick={() => openPaymentModal(project)}
>
Donate
</button> }
<div className="flex items-center justify-center basis-1/2">
<Link href={`/projects/${escapeHTML(slug)}`} passHref legacyBehavior>
<a className="projectlist">View Details</a>
</Link>
<FontAwesomeIcon
icon={faArrowRight}
className="ml-1 w-4 h-4 text-textgray cursor-pointer"
/>
</div>
<Link href={`/${project.fund}/projects/${project.slug}`} passHref target="_blank">
<figure
className={cn(
'max-w-sm min-h-[460px] h-full space-y-2 flex flex-col rounded-xl border-b-4 bg-white',
project.fund === 'monero' && 'border-monero',
project.fund === 'firo' && 'border-firo',
project.fund === 'privacyguides' && 'border-privacyguides',
project.fund === 'general' && 'border-primary'
)}
>
<div className="flex h-36 w-full sm:h-52">
<Image
alt={project.title}
src={project.coverImage}
width={1200}
height={1200}
style={{
objectFit: 'contain',
...customImageStyles,
}}
priority={true}
className="cursor-pointer rounded-t-xl bg-white"
/>
</div>
</figcaption>
</figure>
<figcaption className="p-5 flex flex-col grow space-y-4 justify-between">
<div className="flex flex-col space-y-2">
<div>
<h2 className="font-bold">{project.title}</h2>
<span className="text-sm text-gray-700">by {project.nym}</span>
</div>
<span className="line-clamp-3 text-gray-400">{project.summary}</span>
<span className="font-bold">
Goal: <span className="text-green-500">${numberFormat.format(project.goal)}</span>
</span>
</div>
<Progress
current={
project.totalDonationsBTCInFiat +
project.totalDonationsXMRInFiat +
project.totalDonationsFiat
}
goal={project.goal}
/>
</figcaption>
</figure>
</Link>
)
}

View File

@@ -9,37 +9,23 @@ type ProjectListProps = {
header?: string
exclude?: string
projects: ProjectItem[]
openPaymentModal: (project: ProjectItem) => void
}
const ProjectList: React.FC<ProjectListProps> = ({
header = 'Explore Projects',
exclude,
projects,
openPaymentModal,
}) => {
const [sortedProjects, setSortedProjects] = useState<ProjectItem[]>()
useEffect(() => {
setSortedProjects(projects.filter(p => p.slug !== exclude).sort(() => 0.5 - Math.random()))
}, [projects])
return (
<section className="p-4 md:p-8 flex flex-col items-center">
<div className="flex justify-between items-center pb-8 w-full">
<h1>{header}</h1>
<div className="flex items-center">
<Link href="/projects" className="projectlist">View All</Link>
<FontAwesomeIcon
icon={faArrowRight}
className="ml-1 w-4 h-4 text-textgray cursor-pointer"
/>
</div>
</div>
<ul className="grid md:grid-cols-3 gap-4 max-w-5xl">
{sortedProjects &&
sortedProjects.slice(0, 3).map((p, i) => (
<section className="flex flex-col">
<ul className="mx-auto grid max-w-5xl grid-cols-1 sm:mx-0 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{projects &&
projects.slice(0, 6).map((p, i) => (
<li key={i} className="">
<ProjectCard project={p} openPaymentModal={openPaymentModal} />
<ProjectCard
project={p}
// tags={p.tags}
/>
</li>
))}
</ul>

View File

@@ -0,0 +1,166 @@
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 { useToast } from './ui/use-toast'
import { trpc } from '../utils/trpc'
import { useFundSlug } from '../utils/use-fund-slug'
import Spinner from './Spinner'
const schema = z
.object({
name: z.string().min(1),
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; openLoginModal: () => void }
function RegisterFormModal({ close, openLoginModal }: Props) {
const { toast } = useToast()
const form = useForm<RegisterFormInputs>({ resolver: zodResolver(schema) })
const fundSlug = useFundSlug()
const registerMutation = trpc.auth.register.useMutation()
async function onSubmit(data: RegisterFormInputs) {
if (!fundSlug) return
try {
await registerMutation.mutateAsync({ ...data, fundSlug })
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 (
<>
<DialogHeader>
<DialogTitle>Register</DialogTitle>
<DialogDescription>Start supporting projects today!</DialogDescription>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<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>
)}
/>
<div className="flex flex-row space-x-2">
<Button
type="button"
variant="link"
className="grow basis-0"
onClick={() => (openLoginModal(), close())}
>
I already have an account
</Button>
<Button
type="submit"
disabled={!form.formState.isValid || form.formState.isSubmitting}
className="grow basis-0"
>
{form.formState.isSubmitting && <Spinner />} Register
</Button>
</div>
</form>
</Form>
</>
)
}
1
export default RegisterFormModal

View File

@@ -0,0 +1,13 @@
import { ReactNode } from 'react'
interface Props {
children: ReactNode
}
export default function SectionContainer({ children }: Props) {
return (
<section className="mx-2 max-w-6xl px-4 sm:px-6 lg:mx-auto xl:mx-auto xl:max-w-5xl xl:px-0">
{children}
</section>
)
}

View File

@@ -1,42 +0,0 @@
import { faGithub, faTwitter } from "@fortawesome/free-brands-svg-icons"
import { faLink } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import Link from "next/link"
import escapeHTML from "escape-html"
import { ProjectItem } from "../utils/types"
const ShareButtons: React.FC<{ project: ProjectItem }> = ({ project }) => {
const { git, twitter, website } = project;
return (
<div className="flex space-x-4">
<Link href={escapeHTML(git)} passHref legacyBehavior>
<a className="projectlist">
<FontAwesomeIcon
icon={faGithub}
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
/>
</a>
</Link>
<Link href={`https://twitter.com/${escapeHTML(twitter)}`} passHref legacyBehavior>
<a className="projectlist">
<FontAwesomeIcon
icon={faTwitter}
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
/>
</a>
</Link>
{website && <Link href={escapeHTML(website)} passHref legacyBehavior>
<a className="projectlist">
<FontAwesomeIcon
icon={faLink}
className="w-[2rem] h-[2rem] hover:text-primary cursor-pointer"
/>
</a>
</Link>}
</div>
)
}
export default ShareButtons

View File

@@ -2,14 +2,14 @@ const Spinner = () => {
return (
<svg
role="status"
className="mr-2 w-8 h-8 text-black animate-spin fill-primary"
className="mr-2 w-6 h-6 animate-spin fill-primary"
viewBox="0 0 100 101"
fill="none"
fill="inherit"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
fill="white"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"

View File

@@ -0,0 +1,43 @@
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
import { Button } from './ui/button'
const ThemeSwitch = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme, resolvedTheme } = useTheme()
// When mounted on client, now we can show the UI
useEffect(() => setMounted(true), [])
return (
<Button
aria-label="Toggle Dark Mode"
size="icon"
variant="ghost"
onClick={() =>
setTheme(
theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark'
)
}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="text-gray-900 dark:text-gray-100 h-6 w-6"
>
{mounted && (theme === 'dark' || resolvedTheme === 'dark') ? (
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"
clipRule="evenodd"
/>
) : (
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
)}
</svg>
</Button>
)
}
export default ThemeSwitch

21
components/WebIcon.tsx Normal file
View File

@@ -0,0 +1,21 @@
import { SVGProps } from 'react'
const WebIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={48}
height={48}
fill="none"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
viewBox="0 0 24 24"
{...props}
>
<circle cx={12} cy={12} r={10} />
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20M2 12h20" />
</svg>
)
export default WebIcon

49
components/ui/alert.tsx Normal file
View File

@@ -0,0 +1,49 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '../../utils/cn'
const alertVariants = cva(
'relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-primary [&>svg~*]:pl-7',
{
variants: {
variant: {
default: 'bg-white text-foreground',
destructive:
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
},
},
defaultVariants: {
variant: 'default',
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
))
Alert.displayName = 'Alert'
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn('mb-1 font-semibold leading-none tracking-tight', className)}
{...props}
/>
)
)
AlertTitle.displayName = 'AlertTitle'
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
))
AlertDescription.displayName = 'AlertDescription'
export { Alert, AlertTitle, AlertDescription }

45
components/ui/avatar.tsx Normal file
View File

@@ -0,0 +1,45 @@
import * as React from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { cn } from '../../utils/cn'
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn('relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full', className)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn('aspect-square h-full w-full', className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'flex h-full w-full items-center justify-center rounded-full bg-primary/10',
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

50
components/ui/button.tsx Normal file
View File

@@ -0,0 +1,50 @@
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 gap-2 whitespace-nowrap rounded-md font-semibold text-sm 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-hover',
light: 'text-primary bg-primary/10 hover:bg-primary hover:text-primary-foreground',
destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
outline:
'text-primary border border-primary bg-background shadow-sm hover:bg-primary hover:text-primary-foreground',
ghost: 'hover:bg-primary/20 hover:text-primary-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-10 text-md',
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 }

99
components/ui/dialog.tsx Normal file
View File

@@ -0,0 +1,99 @@
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(
'max-h-dvh fixed left-[50%] top-[50%] z-50 grid w-full overflow-auto sm: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-background 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,
}

View File

@@ -0,0 +1,184 @@
import * as React from 'react'
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
import { CheckIcon, ChevronRightIcon, DotFilledIcon } from '@radix-ui/react-icons'
import { cn } from '../../utils/cn'
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-primary/10 data-[state=open]:bg-primary',
inset && 'pl-8',
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-white p-1 text-foreground 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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md shadow-sm bg-white p-1 text-foreground',
'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-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
'relative flex select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-primary/10 focus:text-primary data-[disabled]:pointer-events-none data-[disabled]:opacity-50 cursor-pointer',
inset && 'pl-8',
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-primary/10 focus:text-primary-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-primary/10 focus:text-primary-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<DotFilledIcon className="h-4 w-4 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn('-mx-1 my-1 h-px bg-border', className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn('ml-auto text-xs tracking-widest opacity-60', className)} {...props} />
}
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

179
components/ui/form.tsx Normal file
View File

@@ -0,0 +1,179 @@
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)
// console.log(fieldState, fieldContext.name)
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, ...rest } = useFormField()
// console.log(error, rest.name)
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,
}

47
components/ui/input.tsx Normal file
View File

@@ -0,0 +1,47 @@
import * as React from 'react'
import { LucideIcon, WebcamIcon } from 'lucide-react'
import { cn } from '../../utils/cn'
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
leftIcon?: LucideIcon
rightIcon?: LucideIcon
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, leftIcon, rightIcon, ...props }, ref) => {
const LeftIcon = leftIcon
const RightIcon = rightIcon
return (
<div className="relative">
{LeftIcon && (
<div className="absolute left-1.5 top-1/2 transform -translate-y-1/2">
<LeftIcon size={18} className="text-primary" />
</div>
)}
<input
type={type}
className={cn(
'flex h-9 w-full rounded-md border bg-white px-3 py-1 text-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:border-primary/70 disabled:cursor-not-allowed disabled:opacity-50',
leftIcon ? 'pl-8' : '',
rightIcon ? 'pr-8' : '',
className
)}
ref={ref}
{...props}
/>
{RightIcon && (
<div className="absolute right-3 top-1/2 transform -translate-y-1/2">
<RightIcon className="text-primary" size={18} />
</div>
)}
</div>
)
}
)
Input.displayName = 'Input'
export { Input }

24
components/ui/label.tsx Normal file
View 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 }

View File

@@ -0,0 +1,42 @@
import * as React from 'react'
import { CheckIcon } from '@radix-ui/react-icons'
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
import { cn } from '../../utils/cn'
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Root
className={cn('grid gap-2', className)}
{...props}
ref={ref}
/>
)
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<CheckIcon className="h-3.5 w-3.5 fill-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }

93
components/ui/table.tsx Normal file
View File

@@ -0,0 +1,93 @@
import * as React from 'react'
import { cn } from '../../utils/cn'
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<div className="relative w-full overflow-auto">
<table ref={ref} className={cn('w-full caption-bottom text-sm', className)} {...props} />
</div>
)
)
Table.displayName = 'Table'
const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tbody ref={ref} className={cn('[&_tr:last-child]:border-0', className)} {...props} />
))
TableBody.displayName = 'TableBody'
const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn('border-t bg-muted/50 font-medium [&>tr]:last:border-b-0', className)}
{...props}
/>
))
TableFooter.displayName = 'TableFooter'
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
'border-b transition-colors hover:bg-muted/20 data-[state=selected]:bg-muted/20',
className
)}
{...props}
/>
)
)
TableRow.displayName = 'TableRow'
const TableHead = React.forwardRef<
HTMLTableCellElement,
React.ThHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
))
TableHead.displayName = 'TableHead'
const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className
)}
{...props}
/>
))
TableCell.displayName = 'TableCell'
const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(({ className, ...props }, ref) => (
<caption ref={ref} className={cn('mt-4 text-sm text-muted-foreground', className)} {...props} />
))
TableCaption.displayName = 'TableCaption'
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }

127
components/ui/toast.tsx Normal file
View 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
View 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
View 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 }

View File

@@ -1,6 +1,7 @@
export const CURRENCY = 'usd'
// Set your amount limits: Use float for decimal currencies and
// Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal.
export const MIN_AMOUNT = 1.0
export const MAX_AMOUNT = 5000.0
export const AMOUNT_STEP = 5.0
export const MIN_AMOUNT = 1
export const MAX_AMOUNT = 5000
export const AMOUNT_STEP = 5
export const MEMBERSHIP_PRICE = 100
export const POINTS_PER_USD = 1
export const POINTS_REDEEM_PRICE_USD = 0.1

18
data/headerNavLinks.ts Normal file
View File

@@ -0,0 +1,18 @@
import { FundSlug } from '@prisma/client'
export const fundHeaderNavLinks: Record<
FundSlug,
{ title: string; href: string; isButton: boolean }[]
> = {
monero: [
{ title: 'Apply', href: 'apply', isButton: false },
{ title: 'FAQs', href: 'faq', isButton: false },
{ title: 'About', href: 'about', isButton: false },
],
firo: [{ title: 'About', href: 'about', isButton: false }],
privacyguides: [{ title: 'About', href: 'about', isButton: false }],
general: [
{ title: 'FAQs', href: 'faq', isButton: false },
{ title: 'About', href: 'about', isButton: false },
],
}

89
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,89 @@
services:
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
restart: unless-stopped
command: tunnel --no-autoupdate run
environment:
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN}
REAL_IP_HEADER: Cf-Connecting-Ip
depends_on:
- nginx
nginx:
image: nginx:1
container_name: nginx
restart: unless-stopped
ports:
- 80:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
depends_on:
- app
app:
build:
context: .
dockerfile: Dockerfile
container_name: app
restart: unless-stopped
environment:
APP_URL: https://donate.magicgrants.org
DATABASE_URL: ${DATABASE_URL}
NEXTAUTH_URL: https://donate.magicgrants.org
NEXTAUTH_URL_INTERNAL: http://localhost:3000
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
USER_SETTINGS_JWT_SECRET: ${USER_SETTINGS_JWT_SECRET}
SMTP_HOST: email-smtp.us-east-2.amazonaws.com
SMTP_PORT: 587
SMTP_USER: ${SMTP_USER}
SMTP_PASS: ${SMTP_PASS}
SES_VERIFIED_SENDER: info@magicgrants.org
STRIPE_MONERO_SECRET_KEY: ${STRIPE_MONERO_SECRET_KEY}
STRIPE_MONERO_WEBHOOK_SECRET: ${STRIPE_MONERO_WEBHOOK_SECRET}
STRIPE_FIRO_SECRET_KEY: ${STRIPE_FIRO_SECRET_KEY}
STRIPE_FIRO_WEBHOOK_SECRET: ${STRIPE_FIRO_WEBHOOK_SECRET}
STRIPE_PRIVACY_GUIDES_SECRET_KEY: ${STRIPE_PRIVACY_GUIDES_SECRET_KEY}
STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET: ${STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET}
STRIPE_GENERAL_SECRET_KEY: ${STRIPE_GENERAL_SECRET_KEY}
STRIPE_GENERAL_WEBHOOK_SECRET: ${STRIPE_GENERAL_WEBHOOK_SECRET}
KEYCLOAK_URL: ${KEYCLOAK_URL}
KEYCLOAK_CLIENT_ID: app
KEYCLOAK_CLIENT_SECRET: ${KEYCLOAK_CLIENT_SECRET}
KEYCLOAK_REALM_NAME: magic
BTCPAY_URL: ${BTCPAY_URL}
BTCPAY_EXTERNAL_URL: https://btcpay.magicgrants.org
BTCPAY_API_KEY: ${BTCPAY_API_KEY}
BTCPAY_STORE_ID: ${BTCPAY_STORE_ID}
BTCPAY_WEBHOOK_SECRET: ${BTCPAY_WEBHOOK_SECRET}
MONERO_APPLICATION_RECIPIENT: monerofund@magicgrants.org
FIRO_APPLICATION_RECIPIENT: monerofund@magicgrants.org
PRIVACY_GUIDES_APPLICATION_RECIPIENT: monerofund@magicgrants.org
GENERAL_APPLICATION_RECIPIENT: monerofund@magicgrants.org
NEXT_PUBLIC_MONERO_APPLICATION_RECIPIENT: monerofund@magicgrants.org
NEXT_PUBLIC_FIRO_APPLICATION_RECIPIENT: monerofund@magicgrants.org
NEXT_PUBLIC_PRIVACY_GUIDES_APPLICATION_RECIPIENT: monerofund@magicgrants.org
NEXT_PUBLIC_GENERAL_APPLICATION_RECIPIENT: monerofund@magicgrants.org
depends_on:
- postgres
postgres:
image: postgres:16-alpine
container_name: postgres
restart: unless-stopped
environment:
POSTGRES_USER: magic
POSTGRES_DB: magic
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- 'postgres_data:/var/lib/postgresql/data'
volumes:
postgres_data:

View File

@@ -1,18 +1,129 @@
version: '3.7'
services:
sample:
container_name: monerofund-frontend-page
build:
context: .
dockerfile: Dockerfile
btcpayserver:
restart: unless-stopped
container_name: generated_btcpayserver_1
image: ${BTCPAY_IMAGE:-btcpayserver/btcpayserver:1.13.3-altcoins}
expose:
- '49392'
environment:
- CHOKIDAR_USEPOLLING=true
stdin_open: true
BTCPAY_POSTGRES: User ID=postgres;Host=btcpay-postgres;Port=5432;Application Name=btcpayserver;Database=btcpayserver${NBITCOIN_NETWORK:-mainnet}
BTCPAY_EXPLORERPOSTGRES: User ID=postgres;Host=btcpay-postgres;Port=5432;Application Name=btcpayserver;MaxPoolSize=80;Database=nbxplorer${NBITCOIN_NETWORK:-mainnet}
BTCPAY_NETWORK: ${NBITCOIN_NETWORK:-mainnet}
BTCPAY_BIND: 0.0.0.0:49392
BTCPAY_ROOTPATH: ${BTCPAY_ROOTPATH:-/}
BTCPAY_SSHCONNECTION: 'root@host.docker.internal'
BTCPAY_SSHTRUSTEDFINGERPRINTS: ${BTCPAY_SSHTRUSTEDFINGERPRINTS}
BTCPAY_SSHKEYFILE: ${BTCPAY_SSHKEYFILE}
BTCPAY_SSHAUTHORIZEDKEYS: ${BTCPAY_SSHAUTHORIZEDKEYS}
BTCPAY_DEBUGLOG: btcpay.log
BTCPAY_UPDATEURL: https://api.github.com/repos/btcpayserver/btcpayserver/releases/latest
BTCPAY_DOCKERDEPLOYMENT: 'true'
BTCPAY_CHAINS: 'xmr'
BTCPAY_XMR_DAEMON_URI: http://xmr-node.cakewallet.com:18081
BTCPAY_XMR_WALLET_DAEMON_URI: http://monerod_wallet:18082
BTCPAY_XMR_WALLET_DAEMON_WALLETDIR: /root/xmr_wallet
labels:
traefik.enable: true
traefik.http.routers.openpico.rule: Host(`monerofund.org`)
traefik.port: 3000
traefik.enable: 'true'
traefik.http.routers.btcpayserver.rule: Host(`${BTCPAY_HOST}`)
extra_hosts:
- 'host.docker.internal:host-gateway'
links:
- btcpay-postgres
volumes:
- 'btcpay_datadir:/datadir'
- 'nbxplorer_datadir:/root/.nbxplorer'
- 'btcpay_pluginsdir:/root/.btcpayserver/Plugins'
- 'xmr_wallet:/root/xmr_wallet'
- 'tor_servicesdir:/var/lib/tor/hidden_services'
- 'tor_torrcdir:/usr/local/etc/tor/'
ports:
- 3000:3000
- '49392:49392'
monerod_wallet:
restart: unless-stopped
container_name: btcpayserver_monero_wallet
image: btcpayserver/monero:0.18.3.3
entrypoint: monero-wallet-rpc --rpc-bind-ip=0.0.0.0 --disable-rpc-login --confirm-external-bind --rpc-bind-port=18082 --non-interactive --trusted-daemon --daemon-address=xmr-node.cakewallet.com:18081 --wallet-file=/wallet/wallet --password-file /wallet/password --tx-notify="/bin/sh ./scripts/notifier.sh -X GET http://btcpayserver:49392/monerolikedaemoncallback/tx?cryptoCode=xmr&hash=%s"
expose:
- '18082'
ports:
- 18082:18082
volumes:
- 'xmr_wallet:/wallet'
nbxplorer:
restart: unless-stopped
container_name: generated_nbxplorer_1
image: nicolasdorier/nbxplorer:2.5.2
expose:
- '32838'
environment:
NBXPLORER_NETWORK: ${NBITCOIN_NETWORK:-mainnet}
NBXPLORER_BIND: 0.0.0.0:32838
NBXPLORER_TRIMEVENTS: 10000
NBXPLORER_SIGNALFILESDIR: /datadir
NBXPLORER_POSTGRES: User ID=postgres;Host=btcpay-postgres;Port=5432;Application Name=nbxplorer;MaxPoolSize=20;Database=nbxplorer${NBITCOIN_NETWORK:-mainnet}
NBXPLORER_AUTOMIGRATE: 1
NBXPLORER_NOMIGRATEEVTS: 1
NBXPLORER_DELETEAFTERMIGRATION: 1
links:
- btcpay-postgres
volumes:
- 'nbxplorer_datadir:/datadir'
btcpay-postgres:
restart: unless-stopped
container_name: generated_postgres_1
shm_size: 256mb
image: btcpayserver/postgres:13.13
command: ['-c', 'random_page_cost=1.0', '-c', 'shared_preload_libraries=pg_stat_statements']
environment:
POSTGRES_HOST_AUTH_METHOD: trust
volumes:
- 'btcpay_postgres_datadir:/var/lib/postgresql/data'
magic-postgres:
image: postgres:16-alpine
container_name: magic-postgres
restart: unless-stopped
ports:
- 5432:5432
environment:
POSTGRES_USER: magic
POSTGRES_DB: magic
POSTGRES_PASSWORD: magic
volumes:
- 'magic_postgres_data:/var/lib/postgresql/data'
keycloak:
image: quay.io/keycloak/keycloak:25.0.6
container_name: magic-keycloak
restart: unless-stopped
command: start-dev
ports:
- 8080:8080
environment:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
campaign-site:
image: node:20-alpine
container_name: magic-campaign-site
restart: unless-stopped
working_dir: /app
command: npm run dev
ports:
- 3000:3000
volumes:
- '.:/app'
volumes:
btcpay_datadir:
btcpay_pluginsdir:
xmr_wallet:
xmr_data:
tor_datadir:
tor_torrcdir:
tor_servicesdir:
nbxplorer_datadir:
btcpay_postgres_datadir:
magic_postgres_data:

9
docs/firo/about_us.md Normal file
View File

@@ -0,0 +1,9 @@
# About the MAGIC Monero Fund
The MAGIC Firo Fund was established in December 2022. It is currently administered by the MAGIC Board. It does not have a committee.
The MAGIC Firo Fund is used to further the Firo and associated ecosystem with research projects, educational projects, and more.
The hope is that the Firo community grows large enough to sustain its own committee for this Fund.
For more information, please email [firofund@magicgrants.org](mailto:firofund@magicgrants.org).

View File

@@ -0,0 +1,49 @@
---
fund: firo
title: 'Aram Jivanyan to Research Elliptic Curves'
summary: "Aram Jivanyan, a researcher with years of research and implementation experience with Firo and Lelantus Spark, is working with MAGIC Grants to conduct six months of additional curve research to improve the privacy of Lelantus Spark."
nym: 'Aram Jivanyan'
coverImage: '/img/project/firo-curve-trees.png'
website: 'https://magicgrants.org/2024/05/15/Aram-Jivanyan-Curve-Tree-Research.html'
personalWebsite: 'https://x.com/aramjivanyan'
socialLinks:
- 'https://x.com/aramjivanyan'
type: 'Other Free and Open Source Project'
date: '2023-05-24'
staticXMRaddress: ''
goal: 40000
isFunded: true
numDonationsXMR: 0
totalDonationsXMRInFiat: 0
totalDonationsXMR: 0
numDonationsBTC: 0
totalDonationsBTCInFiat: 0
totalDonationsBTC: 0
numDonationsFiat: 40000
totalDonationsFiat: 1
---
### Result: [Research paper](https://magicgrants.org/2024/05/15/Aram-Jivanyan-Curve-Tree-Research.html)
Aram Jivanyan, a researcher with years of research and implementation experience with Firo and Lelantus Spark, is working with MAGIC Grants to conduct six months of additional curve research to improve the privacy of Lelantus Spark.
This research grant was made possible because of the [generous donation](https://magicgrants.org/2022/12/22/200000-Donation-from-Arcadia-for-Firo) to the [MAGIC Firo Fund](https://magicgrants.org/funds/firo) by [Arcadia](https://www.arcadiamgroup.com/).
Before working with MAGIC Grants, Aram did preliminary work to assess the feasibility of this research direction. Following this initial analysis, he is confident that this research project is likely to bring greater anonymity sets to the final Lelantus Spark implementation.
The aim of the further 6-month research is the following:
1. Research the most efficient path forward for implementing paired elliptic curve (relating to secP256k1, secQ256k1, and/or other applicable curves). One direction will be implementation of the curve on C++ based on the existing curve implementation. Another possible direction can be bindings of Rust implementation into our C++ library.
2. Nail down how the membership proofs with curve trees will be integrated into the full lelantus spark design and what should be modified to make them interoperable with other spark components.
3. Design the bulletproof-based circuit for set membership checks and nail the implementation details for coding.
4. Write a new paper which will summarizes all research, findings and design for enabling Scaling Lelantus Spark Anonymity Set with Curve Trees.
The research aims to solve the greatest challenge of Spark which is enabling anonymity sets in the millions. Other privacy cryptocurrency protocols such as Monero's Seraphis could potentially benefit of this research as well.
The funding will also cover the Aram's continuing research on the [Aura voting protocol](https://eprint.iacr.org/2022/543) to improve the paper, work on the implementation design and architecture and improve the preprint to get it submitted to crypto conferences.
Stay updated with the MAGIC Grants and Firo social media accounts for updates on this research project.

View File

@@ -0,0 +1,31 @@
---
fund: firo
title: 'Lelantus Spark Flutter Library'
summary: "This library will make it easier to build Flutter applications on Firo Lelantus Spark."
nym: 'Cypher Stack'
coverImage: ''
website: 'https://magicgrants.org/2024/05/16/Firo-Lelantus-Spark-Flutter-Library-Stack-Wallet.html'
personalWebsite: 'https://cypherstack.com'
socialLinks:
- 'https://twitter.com/cypher_stack'
type: 'Other Free and Open Source Project'
date: '2023-05-24'
staticXMRaddress: ''
goal: 13000
isFunded: true
numDonationsXMR: 0
totalDonationsXMRInFiat: 0
totalDonationsXMR: 0
numDonationsBTC: 0
totalDonationsBTCInFiat: 0
totalDonationsBTC: 0
numDonationsFiat: 13000
totalDonationsFiat: 1
---
### Result: [Flutter plugin library](https://github.com/cypherstack/flutter_libsparkmobile)
With Firo's Lelantus Spark [mainnet activation on January 18, 2024](https://firo.org/2024/01/18/spark-is-live.html), the MAGIC Firo Fund tasked Stack Wallet last December and January to build a Flutter plugin for Firo's [sparkmobile](https://github.com/firoorg/sparkmobile) library. [This Flutter plugin](https://github.com/cypherstack/flutter_libsparkmobile) makes it easy for Flutter wallet apps to integrate Firo Lelantus Spark, and because it is MIT-licensed, anyone can use the plugin.
Lelantus Spark is an important evolution in Firo's network. Spark transactions within Spark addresses hide the sender, receiver, and amount transferred.

19
docs/general/about_us.md Normal file
View File

@@ -0,0 +1,19 @@
# About MAGIC Grants
MAGIC Grants is a public charity that provides undergraduate scholarships for students interested in cryptocurrencies and privacy, supports public cryptocurrency infrastructure, and supports privacy.
## Scholarships
MAGIC Grants offers undergraduate scholarships to students who are interested in cryptocurrencies.
## Infrastructure Grants
MAGIC Grants offers infrastructure grants to cryptocurrency ecosystems. These grants can help with security audits, research and development, or community-building.
## Education Grants
MAGIC Grants supports leading educational resources for cryptocurrencies and privacy.
## MAGIC Funds
Semi-autonomous committees raise funds and issue qualifying grants to empower their communities.

26
docs/general/faq.md Normal file
View File

@@ -0,0 +1,26 @@
# FAQ
## What type of charity is MAGIC Grants?
MAGIC Grants is a 501(c)(3) public charity registered in Colorado.
## What are MAGIC Funds?
MAGIC Funds are semi-autonomous dedicated funds that allow for their committees to fund important projects in their respective ecosystems. For more information, check out the [dedicated MAGIC Funds page](https://magicgrants.org/funds).
## How can I apply for a scholarship?
Apply for a scholarship [here](https://magicgrants.org/scholarships/scholarship-application).
## How can I apply for a grant?
Reach out the the MAGIC Fund that you would like to apply for a grant from. If your project does not fit within the scope of an existing MAGIC Fund, please contact [info@magicgrants.org](mailto:info@magicgrants.org).
## How can I contribute to MAGIC Grants?
Your donation is much appreciated! You can contribute a MAGIC Fund or MAGIC Grants at [donate.maigcgrants.org](https://donatemagicgrants.org).
## How can I propose a new MAGIC Fund?
Please review the process to propose a new MAGIC Fund [here](https://magicgrants.org/funds/fund_initial_steps).

View File

View File

@@ -1,6 +1,6 @@
# About the MAGIC Monero Fund
MAGIC (Multidisciplinary Academic Grants in Cryptocurrencies) Grants is a 501(c)(3) U.S. nonprofit that focuses on building strong cryptocurrency communities and networks. [The MAGIC Monero Fund](https://magicgrants.org/funds/monero) is a subentity of MAGIC Grants guided by a five-member committee elected by [Monero community members](https://magicgrants.org/funds/monero/monero_fund_voters/).
MAGIC Grants is a 501(c)(3) U.S. nonprofit that focuses on building strong cryptocurrency communities and networks. [The MAGIC Monero Fund](https://magicgrants.org/funds/monero) is a subentity of MAGIC Grants guided by a five-member committee elected by [Monero community members](https://magicgrants.org/funds/monero/monero_fund_voters).
### MAGIC Monero Fund Vision and Goals:
@@ -21,4 +21,3 @@ MAGIC (Multidisciplinary Academic Grants in Cryptocurrencies) Grants is a 501(c)
* [artlimber](https://github.com/artlimber) is a Monero user and community supporter who believes privacy is a human right.
* [kowalabearhugs](https://twitter.com/kowalabearhugs) is a Monero community member, photographer, artist, seasonal farmer, and offseason wanderer.

View File

@@ -25,7 +25,7 @@ The MAGIC Monero Fund accepts applications on a rolling basis. Applications will
## Proposal Format
Applicants must submit their applications on the web form [here](/apply). Alternatively, applicants can submit a PDF file by email to MoneroFund@magicgrants.org . Applicants are free to use their legal name or a pseudonym at this step, although note the "Eligibility" section below.
Applicants must submit their applications on the web form [here](/monero/apply). Alternatively, applicants can submit a PDF file by email to MoneroFund@magicgrants.org . Applicants are free to use their legal name or a pseudonym at this step, although note the "Eligibility" section below.
The research plan should be 3-5 pages (not counting citations, any images, and biographical sketch) and include the following:
@@ -73,7 +73,7 @@ If your proposal seek to uncover weaknesses in the privacy and/or security featu
## How to Submit an Application
Applicants must submit their applications on the web form [here](/apply). Alternatively, applicants can submit a PDF file by email to MoneroFund@magicgrants.org
Applicants must submit their applications on the web form [here](/monero/apply). Alternatively, applicants can submit a PDF file by email to MoneroFund@magicgrants.org
## Contact

View File

@@ -28,7 +28,6 @@
- We use BTCPay Server to receive all cryptocurrency donations. For fiat donations, we leverage Stripe Payments.
## Can you provide the view key to verify the Fund's incoming Monero donations?
- Yes. You can create a view-only Monero wallet with this information:
@@ -60,7 +59,7 @@
## Are my donations tax deductible?
- Yes. MAGIC Monero Fund is committee under Multidisciplinary Academic Grants in Cryptocurrencies (MAGIC Grants), a 501(c)(3) non-profit organization. All gifts and donations are tax-deductible in the USA.
- Yes, your donation may be tax deductible. MAGIC Monero Fund is committee under MAGIC Grants, a 501(c)(3) non-profit organization. All gifts and donations are tax-deductible in the USA.
- If you would like to receive a tax deduction, we may need to collect supplementary information such as your name and email for required record keeping. If you donate over $500 with cryptocurrencies, you will need to fill out [Form 8283](https://www.irs.gov/pub/irs-pdf/f8283.pdf) and email that form to [info@maigcgrants.org](mailto:info@magicgrants.org) for us to sign Part V.
The Monero Fund relies on donors like you to help promote and support a sustainable ecosystem of contributors to the Monero ecosystem. We greatly appreciate your contributions.
@@ -72,4 +71,3 @@ The Monero Fund relies on donors like you to help promote and support a sustaina
# Can I donate with bank transfer / wire?
- Yes! [Email us](mailto:MoneroFund@magicgrants.org), and we will send you the bank details and manually add your donation. We can accept domestic and international wire transfers. Wires are appreciated for large donations, so that ~3% more of your donation can go to the recipient instead of credit card fees.

View File

@@ -1,53 +1,56 @@
---
fund: monero
title: 'vtnerd Monero and Monero-LWS dev work for Q1/Q2 2024'
summary: "This development work will improve security, performance, and usability with an end goal of helping to broaden the user base. "
summary: 'This development work will improve security, performance, and usability with an end goal of helping to broaden the user base. '
date: '2024-01-26'
nym: 'vtnerd (Lee Clagett)'
website: 'https://github.com/vtnerd'
coverImage: '/img/project/vtnerd-monero-dev.png'
git: 'https://github.com/vtnerd'
twitter: 'vtnerd'
personalTwitter: 'vtnerd'
personalWebsite: 'https://www.leeclagett.com'
website: 'https://github.com/vtnerd'
socialLinks:
- 'https://www.leeclagett.com'
- 'https://x.com/vtnerd'
- 'https://github.com/vtnerd'
type: 'Other Free and Open Source Project'
staticXMRaddress: '8454MvGDuZPFP1WKSMcgbRJqV1sXHz7Z3KyURMpkoLXR3CJUZiebjymjQGc6YvTWqhFZEtJwELbcgFHZ9qGPwPsF7fWLWPT'
goal: 28800
isFunded: true
numdonationsxmr: 43
totaldonationsinfiatxmr: 28800
totaldonationsxmr: 226.1
numdonationsbtc: 0
totaldonationsinfiatbtc: 0
totaldonationsbtc: 0
fiatnumdonations: 0
fiattotaldonationsinfiat: 0
fiattotaldonations: 0
numDonationsXMR: 43
totalDonationsXMRInFiat: 28800
totalDonationsXMR: 226.1
numDonationsBTC: 0
totalDonationsBTCInFiat: 0
totalDonationsBTC: 0
numDonationsFiat: 0
totalDonationsFiat: 0
---
### Funded goal: 28,800 USD
### Start: February 2024
### End: July 2024
### Result: Development Milestones [1](https://github.com/MAGICGrants/Monero-Fund/issues/27), [2](https://github.com/MAGICGrants/Monero-Fund/issues/29), and [3](https://github.com/MAGICGrants/Monero-Fund/issues/31)
vtnerd (Lee Clagett) is the author of [Monero-LWS](https://github.com/vtnerd/monero-lws), and has been a [contributor to the Monero codebase since 2016](https://github.com/monero-project/monero/pulls?page=7&q=is%3Apr+author%3Avtnerd+created%3A%3E2016-10-01). He is a veteran of four CCS proposals; [[1]](https://ccs.getmonero.org/proposals/vtnerd-tor-tx-broadcasting.html), [[2]](https://ccs.getmonero.org/proposals/vtnerd-2020-q4.html), [[3]](https://ccs.getmonero.org/proposals/vtnerd-2021-q1.html), [[4]](https://ccs.getmonero.org/proposals/vtnerd-2023-q3.html)
This proposal funds 480 hours of work, ~3 months. The milestones will be hour based; 160 (1 month), 320 (2 months), 480 (3 months). At the completion of hours, he will provide the Monero Fund committee references to the work that was completed during that timeframe.
This proposal funds 480 hours of work, ~3 months. The milestones will be hour based; 160 (1 month), 320 (2 months), 480 (3 months). At the completion of hours, he will provide the Monero Fund committee references to the work that was completed during that timeframe.
Some features that are being targeted in [`monero-project/monero`](https://www.github.com/monero-project/monero) :
Some features that are being targeted in [`monero-project/monero`](https://www.github.com/monero-project/monero) :
* Get new serialization routine merged (work on piecemeal PRs for reviewers sake) (already in-progress)
* Complete work necessary to merge DANE/TLSA in wallet2/epee.
* Adding trust-on-first-use support to wallet2
- Get new serialization routine merged (work on piecemeal PRs for reviewers sake) (already in-progress)
- Complete work necessary to merge DANE/TLSA in wallet2/epee.
- Adding trust-on-first-use support to wallet2
Work targeted towards [`vtnerd/monero-lws`](https://github.com/vtnerd/monero-lws) :
Work targeted towards [`vtnerd/monero-lws`](https://github.com/vtnerd/monero-lws) :
* Optional full chain verification for malicious daemon attack (already-in progress)
* Webhooks/ZMQ-PUB support for tx sending (watch for unexpected sends)
* ZMQ-pub support for incoming transactions and blocks (notifies of any new transaction or block)
* Implement "horizontal" scaling of account scanning (transfer account info via zmq to another process for scanning)
* Make account creation more "enterprise grade" (currently scanning engine re-starts on every new account creation, and uses non-cacheable memory) * Unit tests for REST-API
* Create frontend LWS C/C++ library
* Provide official LWS docker-image
* Provide official snap/flatpak/appimge (tbd one or all of those)
* Provide pre-built binaries
* (Unlikely) - reproducible builds so community members can verify+sign the binary hashes
* It is unlikely that all features will be implemented, at which point the unfinished features will roll into the next quarter.
- Optional full chain verification for malicious daemon attack (already-in progress)
- Webhooks/ZMQ-PUB support for tx sending (watch for unexpected sends)
- ZMQ-pub support for incoming transactions and blocks (notifies of any new transaction or block)
- Implement "horizontal" scaling of account scanning (transfer account info via zmq to another process for scanning)
- Make account creation more "enterprise grade" (currently scanning engine re-starts on every new account creation, and uses non-cacheable memory) \* Unit tests for REST-API
- Create frontend LWS C/C++ library
- Provide official LWS docker-image
- Provide official snap/flatpak/appimge (tbd one or all of those)
- Provide pre-built binaries
- (Unlikely) - reproducible builds so community members can verify+sign the binary hashes
- It is unlikely that all features will be implemented, at which point the unfinished features will roll into the next quarter.

View File

@@ -1,31 +1,35 @@
---
fund: monero
title: 'Research to Defeat EAE Attack and Analyze Effectiveness of Churning Procedures'
summary: "The EAE attack is one of Monero's privacy weak points. Churning may be a solution."
nym: 'Dr. Nathan Borggren'
website: 'https://github.com/nborggren'
coverImage: '/img/project/EAE_diagram.png'
git: 'https://github.com/nborggren'
twitter: 'magicgrants'
personalTwitter: 'magicgrants'
website: 'https://github.com/nborggren'
personalWebsite: 'https://github.com/nborggren'
socialLinks:
- 'https://github.com/nborggren'
type: 'Other Free and Open Source Project'
date: '2023-06-08'
staticXMRaddress: '87LZA8XLDvhVKLi974MaxUANcvkWdL6n986R7WNgKLXY16y31t69Z8228EWcg8THQq3tuAWfQ7Np35Tt3AhPrjzcNbm8Jr5'
goal: 29260
isFunded: true
numdonationsxmr: 27
totaldonationsinfiatxmr: 29260
totaldonationsxmr: 220
numdonationsbtc: 0
totaldonationsinfiatbtc: 0
totaldonationsbtc: 0
fiatnumdonations: 0
fiattotaldonationsinfiat: 0
fiattotaldonations: 0
numDonationsXMR: 27
totalDonationsXMRInFiat: 29260
totalDonationsXMR: 220
numDonationsBTC: 0
totalDonationsBTCInFiat: 0
totalDonationsBTC: 0
numDonationsFiat: 0
totalDonationsFiat: 0
---
### Funded Goal: 220 XMR (86 XMR contributed from MAGIC Monero Fund general fund)
### Start: June 2023
### End: September 2023
### Result: [Research paper](/pdf/Borggren-Sept-2023-Probing-the-Attacks-on-the-Privacy-of-the-Monero-Blockchain.pdf)
The EAE (Eve-Alice-Eve) attack is a threat to Monero user privacy. When a Monero user sends and/or receives funds repeatedly with an entity (Eve) that aims to trace funds, enough information in the local transaction graph and ring signatures may be available to make a probabalistic guess about the true destination or source of funds. There are different names for this attack and related attacks, which mostly differ in how many colluding entities are involved and how much information they may possess: EABE, poisoned outputs, Overseer, Flashlight, Tainted Dust, and Knacc attacks. One of the videos of the Breaking Monero series [discusses the EAE attack in detail](https://www.monerooutreach.org/breaking-monero/poisoned-outputs.html).
@@ -36,8 +40,7 @@ Dr. Nathan Borggren has stepped up to investigate these important research quest
Dr. Borggren has proposed using Topological Data Analysis and Bayesian statistical methods to analyze the EAE attack and related attacks on Monero user privacy. The false positive and false negative rate of the EAE attack will be investigated through simulations and possibly analysis of Monero's mainnet blockchain. The full details of the plan are in [the research proposal](https://github.com/MAGICGrants/Monero-Fund/blob/main/projects/borggren_research-MAGIC-submission.pdf).
> "I think that the EAE attack is one of Monero's biggest *practical* attack surfaces currently, and I see value in quantification plus real world data-informed best practices."
> "I think that the EAE attack is one of Monero's biggest _practical_ attack surfaces currently, and I see value in quantification plus real world data-informed best practices."
&mdash; [isthmus](https://github.com/Mitchellpkt), Monero Research Lab researcher and lead author of ["Fingerprinting a flood: Forensic statistical analysis of the mid-2021 Monero transaction volume anomaly"](https://mitchellpkt.medium.com/fingerprinting-a-flood-forensic-statistical-analysis-of-the-mid-2021-monero-transaction-volume-a19cbf41ce60)

View File

@@ -1,29 +1,35 @@
---
fund: monero
title: 'ETH<>XMR Atomic Swap Continued Development'
summary: "A trustless way to exchange Monero and Ethereum."
summary: 'A trustless way to exchange Monero and Ethereum.'
nym: 'noot'
website: 'https://github.com/AthanorLabs/atomic-swap'
date: '2022-09-01'
coverImage: '/img/project/Ethereum_logo.png'
git: 'https://github.com/noot'
twitter: 'elizabethereum'
personalTwitter: 'elizabethereum'
website: 'https://github.com/AthanorLabs/atomic-swap'
personalWebsite: 'https://github.com/noot'
socialLinks:
- 'https://x.com/elizabethereum'
- 'https://github.com/noot'
type: 'Other Free and Open Source Project'
goal: 24000
isFunded: true
numdonationsxmr: 66
totaldonationsinfiatxmr: 18861
totaldonationsxmr: 125.74
numdonationsbtc: 0
totaldonationsinfiatbtc: 0
totaldonationsbtc: 0
fiatnumdonations: 8
fiattotaldonationsinfiat: 5139
fiattotaldonations: 5139
numDonationsXMR: 66
totalDonationsXMRInFiat: 18861
totalDonationsXMR: 125.74
numDonationsBTC: 0
totalDonationsBTCInFiat: 0
totalDonationsBTC: 0
numDonationsFiat: 8
totalDonationsFiat: 5139
---
### Funded Goal: 24,000 USD (5,000 USD contributed from the MAGIC Monero Fund general fund)
### Start: September 2022
### End: August 2022
### Result: [Mainnet beta software release](https://reddit.com/r/Monero/comments/1382rva/ethxmr_atomic_swap_beta_release/)
The MAGIC Monero Fund raised funds for noot to continue development on ETH-XMR atomic swaps. View [the campaign here](https://www.gofundme.com/f/noot-ethxmr-atomic-swap-development-4-months).
@@ -51,6 +57,3 @@ The current implementation of the swap does not store anything to disk apart fro
### General maintenance and bugfixes
See https://github.com/noot/atomic-swap/issues for open issues on the repo. Issues not covered by the above work are part of this sec-tion. This includes RPC calls and documentation, codebase maintenance, testing, and fixes of any bugs found during testing.

View File

@@ -1,29 +1,35 @@
---
fund: monero
title: 'Ring Signature Resiliency to AI Analysis'
summary: "A test of machine learning attacks on Monero's untraceability."
nym: 'ACK-J'
website: 'https://magicgrants.org/Monero-Tracing-Research/'
date: '2022-03-01'
coverImage: '/img/project/ring_sig.png'
git: 'https://github.com/ACK-J'
twitter: 'G666g1e'
personalTwitter: 'G666g1e'
website: 'https://magicgrants.org/Monero-Tracing-Research/'
personalWebsite: 'https://github.com/ACK-J'
socialLinks:
- 'https://x.com/G666g1e'
- 'https://github.com/ACK-J'
type: 'Other Free and Open Source Project'
goal: 12000
isFunded: true
numdonationsxmr: 1
totaldonationsinfiatxmr: 12000
totaldonationsxmr: 80
numdonationsbtc: 0
totaldonationsinfiatbtc: 0
totaldonationsbtc: 0
fiatnumdonations: 0
fiattotaldonationsinfiat: 0
fiattotaldonations: 0
numDonationsXMR: 1
totalDonationsXMRInFiat: 12000
totalDonationsXMR: 80
numDonationsBTC: 0
totalDonationsBTCInFiat: 0
totalDonationsBTC: 0
numDonationsFiat: 0
totalDonationsFiat: 0
---
### Funded Goal: 12,000 USD (12,000 USD contributed from MAGIC Monero Fund general fund)
### Start: March 2022
### End: August 2022
### Result: [Research paper](https://raw.githubusercontent.com/ACK-J/Monero-Dataset-Pipeline/main/Lord_of_the_Rings__An_Empirical_Analysis_of_Monero_s_Ring_Signature_Resilience_to_Artificially_Intelligent_Attacks.pdf)
ACK-J created a series of Monero transactions using different spend patterns and applied artificial intelligence models to determine the resiliency of ring signatures against these models, absent external information. The MAGIC Monero Fund approved the research grant from its general fund.
@@ -35,6 +41,3 @@ The transcript of a short inverview with ACK-J discussing the results is [here](
## Main results
With 11 ring members, public information on the Monero blockchain could aid an attacker in predicting the true spend of a transaction greater than the random guessing probability of 9% (1/11). With this model, the likelihood of a correct guess grew to 13.3%, a modest increase. Since the data was collected, Monero increased its ring size to 16; thus, the accuracy should now be lower, but we do not have numbers for this.

View File

@@ -1,75 +1,51 @@
# Privacy Policy
# MAGIC Grants Privacy Policy
**Last Updated: November 5, 2021**
**Last Updated: October 14, 2024**
Open Sats Initiative&#39;s core purpose is to foster an ecosystem that supports the collaborative and public development of free and open-source software projects (each, a &quot;Project&quot;). This privacy policy (&quot;Privacy Policy&quot;) describes our policies and procedures about the collection, use, disclosure and sharing, or other processing of your personal information when you use our websites (e.g., opensats.org) or participate in or use our project sites (collectively, the &quot;Sites&quot;), as well as when you interact with or participate in our educations and training programs and events. (collectively, the &quot;Services&quot;). This Privacy Policy applies to activities by Open Sats Initiative and its affiliates, subsidiaries and related entities (collectively &quot;Open Sats&quot; &quot;we&quot; or &quot;us&quot;), including activities that we perform for other entities through third party agreements.
MAGIC Grants's is a Colorado public charity.
For purposes of the General Data Protection Regulation (&quot;GDPR&quot;), Open Sats is the controller of your personal information. Where processing of personal information is undertaken by our affiliates, subsidiaries and related entities, they are a joint controller with Open Sats Initiative for your personal information.
This privacy policy (**Privacy Policy**) describes our policies and procedures about the collection, use, disclosure and sharing, or other processing of your personal information when you use our websites (e.g., magcigrants.org) or participate in or use our project sites (collectively, the **Sites**), as well as when you interact with or participate in our educations and training programs and events (collectively, the **Services**). This Privacy Policy applies to activities by MAGIC Grants and its affiliates, subsidiaries and related entities (collectively **MAGIC Grants**; **we**, or **us**), including activities that we perform for other entities through third party agreements.
Capitalized terms that are not defined in this Privacy Policy have the meaning given them in our Terms and Conditions (the &quot;Terms&quot;). In this Privacy Policy, &quot;personal information&quot; includes references to &quot;personal data&quot; as defined under applicable laws. Your use of our Sites and Services, and any dispute over privacy, is subject to this Policy and the relevant Terms, including the applicable limitations on damages and the resolution of disputes. The Terms are incorporated by reference into this Policy.
For purposes of the General Data Protection Regulation (**GDPR**), MAGIC Grants is the controller of your personal information. Where processing of personal information is undertaken by our affiliates, subsidiaries and related entities, they are a joint controller with MAGIC Grants for your personal information.
## Personal Information That Open Sats Collects
Capitalized terms that are not defined in this Privacy Policy have the meaning given them in our Terms and Conditions (the **Terms**). In this Privacy Policy, **personal information** includes references to **personal data** as defined under applicable laws. Your use of our Sites and Services, and any dispute over privacy, is subject to this Policy and the relevant Terms, including the applicable limitations on damages and the resolution of disputes. The Terms are incorporated by reference into this Policy.
We collect personal information directly from individuals, from third parties, and automatically through the Sites and Services. You do not have to provide us your personal information. However, if you choose not to disclose certain information, we will not be able to provide you with access to certain services or features, including registering on the Sites, registration for training, events, or other programs, or participation in certain aspects of our open-source projects.
## Summary
_Registration Information._ We collect personal information when you register for an account or register to participate in an Open Sats event or Program:
MAGIC Grants aims to collect the least information required. You may provide a name and email address to create an account with our Sites and Services. We do not share these with third parties except as required by law. We retain records of donor and grant information as required by law.
_Account and Profile Information._ Users may sign up for, request, or order our Services and may register to receive materials on our Sites. Users may also create a login, which is a single sign-on account which is used for common access to many of our Services. Personal information collected on the Sites includes community forum content, profiles, photographs, names, unique identifiers, information about your current and past employment affiliations, contact information (address, telephone number, email address, preferred pronoun, etc.), and transaction information (to the extent you share that information). In order to access certain personalized services on the Sites, you may be asked to also create and store a username and password for an account from Open Sats. In order to improve the functioning of the website and our subsequent communications to users we may also ask users to provide additional optional information regarding your interests, demographics, experience and detailed contact preferences.
You may subscribe to our email service, which you can opt out of at any time.
_Donations._ To register for and participate in the Open Sats Projects and related Services, users must have an active login ID and account (see above for information collected related to account registration). Open Sats may collect the following information related to financial contributions and :Depending on the Community Bridge Services in which users participate, we may also collect additional information relating to their use of those Services, including:
This Privacy Policy does not cover your interactions with third parties, such as you joining a Discord server or Matrix channel to communicate with MAGIC Grants community members. The third party's privacy policy will apply.
- _Donations_: We collect information about financial contributions made, as well as and funds received through the Sites. Open Sats generally only records the result of the transaction and any references to the transaction record provided by the third-party site. For example, when users make financial contributions to projects, we collect and process the donation amount, allocation to certain projects, and identifiers used to associate that donation with the donor and project in the project&#39;s open and transparent public ledger (unless otherwise agreed to by the donor and Open Sats. We also collect disbursement amount and category, recipient name and email, and identifiers related to disbursements of project funds for projects. Further, we use third-party services including Stripe to facilitate funding and disbursements. If applicable, the third-party site may collect payment information directly to facilitate a transaction.
If you have questions about this Privacy Policy, please contact info@magicgrants.org.
_Events Registration._ When you register for an Open Sats event (training, conference, or other event) to participate as an attendee, a speaker or a sponsor, we collect personal information that includes name, company, contact information, and other information. We may also collect other optional personal information such as likes, interests, preferred pronoun, dietary restriction, size preferences for conference attire gifts and other background information. In addition, if you provide it, we may collect (1) personal information about disabilities, medical conditions and allergies in order to provide appropriate accommodations for attendees, and (2) personal information about your citizenship, date of birth, and passport details if you request assistance from us with obtaining a visa letter to travel to one of our events.
## Personal Information That MAGIC Grants Collects
For in-person events requiring attendees to be vaccinated against COVID-19, in order to provide a safer environment for attendees and staff, we may collect information to verify your identity and COVID-19 vaccination status. We may collect this information via direct verification of identity and vaccination status documents by Open Sats staff or third-party contractors, and/or through the use of third-party vaccination status apps and service providers.
We collect personal information directly from individuals, from third parties, and automatically through the Sites and Services. You do not have to provide us your personal information. However, if you choose not to disclose certain information, that may limit your eligibility for certain products and services.
_Training and Certification Exam Registration_. When you participate in one of our training or certification programs, we collect registration-related personal information that includes name, company, certifications, contact information, and other information depending on the circumstances.
_Registration Information._ We collect personal information when you register for an account, or when you provide it when filling out a form.
_Registration for Projects._ You can register to receive access to various information provided by Open Sats and its free and open-source Projects relating to the open-source ecosystem, open source project development, collaboration and best practices. This includes providing us with personal information such as your email address and name to receive newsletters, mailing list postings and social media postings, to view webinars, and to access other resources made available by Open Sats and its Projects.
_Account and Profile Information._ Users may sign up for, request, or order our Services and may register to receive materials on our Sites. Users may also create a login, which is a single sign-on account which is used for common access to many of our Services. Personal information collected on the Sites may include community forum content, profiles, photographs, names, unique identifiers, information about your current and past employment affiliations, contact information (address, telephone number, email address, preferred pronoun, etc.), and transaction information (to the extent you share that information). In order to access certain personalized services on the Sites, you may be asked to also create and store a username and password for an account from MAGIC Grants. In order to improve the functioning of the website and our subsequent communications to users we may also ask users to provide additional optional information regarding your interests, demographics, experience and detailed contact preferences.
**Your Contributions to Open Source Projects**.
_Donations._ To register for and participate in select MAGIC Grants Projects and related Services, users may need an login ID and account (see above for information collected related to account registration). MAGIC Grants may collect the following information related to financial contributions, and we may also collect additional information relating to their use of those Services. Your donation records are kept by MAGIC Grants so that we can meet our charitable requirements.
_Project Integrity and Credit for Attribution_. When you contribute source code, documentation or other content to one of our Projects (whether on your own behalf or through a third party entity), we collect and store the information and content that you contribute. This includes the contents of those contributions, as well as information required to confirm the provenance of intellectual property contained in those contributions, and personal information that you make publicly available in the record of the contribution pursuant to sign-offs under the a Certificate of Origin as Follows:
_Training and Certification Exam Registration_. If you participate in any training and/or certification program, we may collect information about you such as your name, employer, address, and other information that you provide.
_Developer&#39;s Certificate of Origin:_
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I have the right to submit it under the open-source license indicated in the file; or
(b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open-source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or
(c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it.
(d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. [end]
Some Projects require additional agreements or information pursuant to their intellectual property policies; in such cases we collect and store information related to your acceptance of those agreements. We may also collect information relating to your participation in technical, governance, or other Project-related meetings.
_Other Project-related Content._ The content you provide in relation to Projects also includes materials that you make publicly available in connection with Project development, collaboration and communication, such as on mailing lists, blogs, Project pages and issue trackers, and related services.
_Your Content_. We collect and store the information and content that you post to the Sites, including your questions, answers, comments, forum postings, and responses to surveys.
_Communications_. When you communicate with us (via email, phone, through the Sites or otherwise), we may maintain a record of your communication.
_Payment Information._ To participate in or purchase Services (including registering for events, training and certification exams), users may be asked to be directed to a third-party site, such as Stripe, to pay for their purchases. If applicable, the third-party site may collect payment information directly to facilitate a transaction. Open Sats generally only records the result of the transaction and any references to the transaction record provided by the third-party site.
_Automatically Collected Information._ In addition, Open Sats may automatically collect the following information about users&#39; use of the Sites or Services through cookies, web beacons, and other technologies: your domain name; your browser type and operating system; web pages you view; when you open certain emails we send; links you click; your IP address; your country of location; the length of time you visit our Sites and or use our Services; and the referring URL, or the webpage that led you to our Sites. We may combine this information with other information that we have collected about you, including, where applicable, your user name, name, and other personal information.
_Membership_. If you create an account and become a member, we collect and retain information necessary for the maintenance of your membership.
**Basis for Use of Personal Information:**
**Purposes and Legitimate Interests**
Open Sats uses the personal information we collect for our legitimate non-proft and charitable business interests, which include the following purposes:
MAGIC Grants uses the personal information we collect for our legitimate non-proft and charitable business interests, which include the following purposes:
- _Providing our Sites and Services_. To provide the Services and our Sites, to communicate with you about your use of our Sites and Services, to respond to your inquiries, provide support and maintenance of the Sites and for other purposes to support users and the community.
- _Operating our Open-Source Projects_. To enable communication between and among open source developers in the community; to facilitate and document Project governance and technical decision-making; to maintain, and make publicly available on a perpetual basis, records regarding intellectual property provenance and license compliance for Project contributions; and for related activities to further Open Sats&#39;s core purpose of fostering an ecosystem that supports the collaborative and public development of free and open source software projects. See the &quot;Project Integrity and Credit for Attribution&quot; section above for more information.
- _Operating our Open-Source Projects_. To enable communication between and among open source developers in the community; to facilitate and document Project governance and technical decision-making; to maintain, and make publicly available on a perpetual basis, records regarding intellectual property provenance and license compliance for Project contributions; and for related activities to further MAGIC Grants's core purposes.
- _Maintain our Training and Certification Programs_. To maintain records about who has attended or registered to attend educational or training programs.
- _Event Administration_. To plan, organize, and facilitate access to events and related services and activities, and to carry out informative and safe events for participants, including attendees, speakers and sponsors. If you provide us information about disabilities, medical conditions and allergies, we will use this information in order to provide appropriate accommodations for attendees and to ensure their health and safety; we will not use this information for other purposes, unless required by law or as necessary to defend our legal rights. For in-person events requiring attendees to be vaccinated against COVID-19, we use information regarding your COVID-19 vaccination status to provide a safer environment for attendees and staff, in order to confirm vaccination status before permitting access to the event venue space.
- _Event Administration_. To plan, organize, and facilitate access to events and related services and activities, and to carry out informative and safe events for participants, including attendees, speakers and sponsors. If you provide us information about disabilities, medical conditions and allergies, we will use this information in order to provide appropriate accommodations for attendees and to ensure their health and safety; we will not use this information for other purposes, unless required by law or as necessary to defend our legal rights.
- _Personalization_. To tailor the content and information that we may send or display to you on our Sites and in our Services, to offer location customization and personalized help and instructions and to otherwise personalize your experiences.
- _Marketing and Promotions_. For marketing and promotional purposes, such as to send you news and newsletters, special offers, and promotions, or to otherwise contact you about Projects, Services, events, trainings or other information we think may interest you related to Open Sats, and, subject to applicable law, our affiliates, subsidiaries and managed services entities.
- _Advertising_. For targeting advertising to you on our Sites and third-party sites and measuring the effectiveness and reach of ads and services (through third-party ad networks and services).
- _Analytics_. To gather metrics to better understand how users access and use our Sites and Services and participate in our Projects; to evaluate and improve the Sites, including personalization, to develop new services; and to understand metrics regarding the community health of our Projects. If a user voluntary provides and explicitly consents to our processing of personal information regarding their demographics and socioeconomics, we process such personal information for the specific purposes for which you have consented, which may include for the purpose of compiling, analyzing and disclosing aggregate statistics regarding diversity of participation in open source projects and communities.
- _Marketing and Promotions_. For marketing and promotional purposes, such as to send you news and newsletters, special offers, and promotions, or to otherwise contact you about Projects, Services, events, trainings or other information we think may interest you related to MAGIC Grants, and, subject to applicable law, our affiliates, subsidiaries and managed services entities.
- _Compliance_. To comply with legal obligations and requests. For example, to comply with laws that compel us to disclose information to public authorities, courts, law enforcement or regulators, maintain records for a certain period, or maintain records demonstrating enforcement and sublicensing of our trademarks and those of our Projects.
- _Business and Legal Operations_. As part of our general charitable and non-profit business and legal operations (e.g., accounting, record keeping, and for other business administration purposes), and as necessary to establish, exercise and defend (actual and potential) legal claims.
- _Prevent Misuse_. Where we believe necessary to investigate, prevent or take action regarding illegal activities, suspected fraud, situations involving potential threats to the safety of any person or violations of the relevant Terms and Conditions or this Privacy Policy.
@@ -80,11 +56,11 @@ We disclose personal information as set forth below, and where individuals have
- _Publicly Available Information, including Your Contributions to Open-Source Projects._ User names, other user ids, email addresses and other attribution information related to the information and contributions that a user posts in conjunction with or subject to an Open Source license are publicly available in the relevant Project source code repositories. Your contributions to Open-Source Projects, and certain of your other Content such as comments and messages posted to public forums, are available to other participants and users of our Projects and of our Services, and may be viewed publicly. In some cases you may be able to provide Project or contribution-related information directly to third-party sites and services; these third parties are independent data controllers and their use of your personal information is subject to their own policies.
- _Service Providers._ We may share your information with third party service providers who use this information to perform services for us, such as payment processors, hosting providers, auditors, advisors, contractors and consultants.
- _Affiliates._ The information collected about you may be accessed by or shared with subsidiaries and affiliates of Open Sats, whose use and disclosure of your personal information is subject to this Privacy Policy, unless an affiliate has its own separate privacy policy.
- _Event Participants_. If you register for an event, we may ask for your consent to share your personal information with third party sponsors and other participants. We will not share your event information with third parties without your consent. For in-person events requiring attendees to be vaccinated against COVID-19, we may use third-party service providers to validate your identity and COVID-19 vaccination status.
- _Affiliates._ The information collected about you may be accessed by or shared with subsidiaries and affiliates of MAGIC Grants, whose use and disclosure of your personal information is subject to this Privacy Policy, unless an affiliate has its own separate privacy policy.
- _Event Participants_. If you register for an event, we may ask for your consent to share your personal information with third party sponsors and other participants. We will not share your event information with third parties without your consent.
- _Training and Program Sponsors_. If you participate in one of our education, certification, or training programs that a third party has sponsored or engaged us to provide to you and others (for example, your employers), we may receive attendee list information from them and may share information about your completion of the program, including confirmation of your participation and your certification exam results, as applicable; these third parties are independent data controllers and their use of your personal information is subject to their own policies. You may also elect to provide third parties (e.g., your employers or your prospective employers) with information that will enable them to look up your certification exam status; if you do so, we may share your certification exam status with such third parties.
- _Legally Required._ We may disclose your information if we are required to do so by law (including to law enforcement in the U.S. and other jurisdictions).
- _Protection of Rights._ We may disclose information where we believe it necessary to respond to claims asserted against us or, comply with legal process (e.g., subpoenas or warrants), enforce or administer our agreements and terms, for fraud prevention, risk assessment, investigation, and protect the rights, property or safety of Open Sats, its Users, participants in its events or Projects, or others.
- _Protection of Rights._ We may disclose information where we believe it necessary to respond to claims asserted against us or, comply with legal process (e.g., subpoenas or warrants), enforce or administer our agreements and terms, for fraud prevention, risk assessment, investigation, and protect the rights, property or safety of MAGIC Grants, its Users, participants in its events or Projects, or others.
- _Anonymized and Aggregated Information._ We may share aggregated information with third parties for research, marketing, analytics and other purposes, provided such information does not identify a particular individual.
**Cookies, Tracking, and Interest-Based Ads**
@@ -92,20 +68,17 @@ We disclose personal information as set forth below, and where individuals have
We and our third-party providers use cookies, pixel tags, JavaScript, log files, and other mechanisms to automatically collect and record information about your usage and browsing activities on our Site and across third party sites or online services. We may combine this information with other information we collect about users. Below, we provide a brief summary these activities.
- _Cookies._ These are small files with a unique identifier that are transferred to your browser through our websites. They allow us to remember users who are logged in, to understand how users navigate through and use the Sites, and to display personalized content and targeted ads (including on third party sites and applications).
- _Pixels, web beacons, clear GIFs_ These are tiny graphics with a unique identifier, similar in function to cookies, which we track browsing activities.
- _Analytics Tools._ We may use internal and third-party analytics tools. The third-party analytics companies we work with may combine the information collected with other information they have independently collected from other websites and/or other online products and services. Their collection and use of information is subject to their own privacy policies.
**Targeted Ads.** As discussed in our Cookie Policy, we may work with third-party advertisers to display more relevant ads on our website and on third party sites; these third parties may display ads to you based on your visit to our Sites and other third-party sites.
**Data Security**
We have implemented commercially reasonable precautions designed to protect the information we collect from loss, misuse, and unauthorized access, disclosure, alteration, and destruction. Please be aware that despite our best efforts, Open Sats provides no guarantee that any security measure will be completely and totally secure.
We have implemented commercially reasonable precautions designed to protect the information we collect from loss, misuse, and unauthorized access, disclosure, alteration, and destruction. Please be aware that despite our best efforts, MAGIC Grants provides no guarantee that any security measure will be completely and totally secure.
We are not responsible for any lost, stolen, or compromised passwords or for any activity on your account via unauthorized password activity. We ask you to promptly notify us if you become aware that any information provided by or submitted to our Sites or through our Services is lost, stolen, or used without permission at info@opensats.org.
We are not responsible for any lost, stolen, or compromised passwords or for any activity on your account via unauthorized password activity. We ask you to promptly notify us if you become aware that any information provided by or submitted to our Sites or through our Services is lost, stolen, or used without permission at info@magicgrants.org.
**Marketing Choices**
You may opt out of or withdraw your consent to receive direct marketing emails from us by using the unsubscribe or opt out mechanisms included in our marketing emails or by emailing info@opensats.org. You may also unsubscribe from mailing lists via the applicable mailing list&#39;s subscription website or, in some cases, by using the unsubscribe mechanisms included in such emails.
You may opt out of or withdraw your consent to receive direct marketing emails from us by using the unsubscribe or opt out mechanisms included in our marketing emails or by emailing info@magicgrants.org. You may also unsubscribe from mailing lists via the applicable mailing list's subscription website or by using the unsubscribe mechanisms included in such emails.
**Retention of Your Personal Information**
@@ -113,13 +86,13 @@ We generally keep personal information only for as long as required to fulfill t
**International Information:**
**Scope**. This section applies to individuals in the European Union &quot;EU&quot; (for these purposes, reference to the EU also includes the European Economic Area countries of Iceland, Liechtenstein and Norway and, to the extent applicable, Switzerland).
**Scope**. This section applies to individuals in the European Union **EU** (for these purposes, reference to the EU also includes the European Economic Area countries of Iceland, Liechtenstein and Norway and, to the extent applicable, Switzerland).
**Data Controller**. Open Sats is the data controller for the processing of Personal data related to donor and non-profit accounts and information on our Sites. You can find our contact information below.
**Data Controller**. MAGIC Grants is the data controller for the processing of Personal data related to donor and non-profit accounts and information on our Sites. You can find our contact information below.
Open Sats is the data processor with respect to processing personal information related to donations and any interaction with Projects. If you wish to exercise one of the below rights with respect to your contribution (whether financial or to a Project) please contact us.
MAGIC Grants is the data processor with respect to processing personal information related to donations and any interaction with Projects. If you wish to exercise one of the below rights with respect to your contribution (whether financial or to a Project) please contact us.
**Your Rights**. Pursuant to the GDPR, to the extent Open Sats is a data controller of your Personal data, you have the following rights in relation to your personal data, under certain circumstances:
**Your Rights**. Pursuant to the GDPR, to the extent MAGIC Grants is a data controller of your Personal data, you have the following rights in relation to your personal data, under certain circumstances:
**Right of access** : If you ask us, we will confirm whether we are processing your personal data and, if so, provide you with a copy of that personal data along with certain other details. If you require additional copies, we may need to charge a reasonable fee.
@@ -127,7 +100,7 @@ Open Sats is the data processor with respect to processing personal information
**Right to erasure** : You may ask us to delete or remove your personal data, such as where you withdraw your consent. If we shared your data with others, we will tell them about the erasure where possible. If you ask us, and where possible and lawful to do so, we will also tell you with whom we shared your Personal data with so you can contact them directly.
**Right to restrict processing** : You may ask us to restrict or &#39;block&#39; the processing of your personal data in certain circumstances, such as where you contest the accuracy of the data or object to us processing it (please read below for information on your right to object). We will tell you before we lift any restriction on processing. If we shared your Personal data with others, we will tell them about the restriction where possible. If you ask us, and where possible and lawful to do so, we will also tell you with whom we shared your Personal data so you can contact them directly.
**Right to restrict processing** : You may ask us to restrict or **block** the processing of your personal data in certain circumstances, such as where you contest the accuracy of the data or object to us processing it (please read below for information on your right to object). We will tell you before we lift any restriction on processing. If we shared your Personal data with others, we will tell them about the restriction where possible. If you ask us, and where possible and lawful to do so, we will also tell you with whom we shared your Personal data so you can contact them directly.
**Right to data portability** : You have the right to obtain your personal data from us that you consented to give us or that was provided to us as necessary in connection with our contract with you, and that is processed by automated means. We will give you your personal data in a structured, commonly used and machine-readable format. You may reuse it elsewhere.
@@ -139,33 +112,33 @@ If we are relying on a legitimate interest to process your personal data (unless
**Right to lodge a complaint with the data protection authority** : If you have a concern about our privacy practices, including the way we handled your personal data, you can report it to the data protection authority that is authorized to hear those concerns.
Please see the section below with our contact information on how to reach Open Sats to exercise your rights.
Please see the section below with our contact information on how to reach MAGIC Grants to exercise your rights.
**Legitimate Interest**. &quot;Legitimate interests&quot; means our interests in conducting our charitable business and developing a relationship with you. This Privacy Policy describes when we process your Personal data for our legitimate interests, what these interests are and your rights. We will not use your Personal data for activities where the impact on you overrides our interests, unless we have your consent, or those activities are otherwise required or permitted by law.
**International Transfers of Personal Data**. Because Open Sats is a non-profit organization that is not subject to the jurisdiction of the United States Federal Trade Commission, it is not eligible for certification under the EU-U.S. and Swiss-U.S. Privacy Shield frameworks (&quot;Frameworks&quot;) as set forth by the U.S. Department of Commerce regarding the processing of personal data transferred from the EU, United Kingdom, and Switzerland to the U.S. (for these purposes, reference to the EU also includes the European Economic Area countries of Iceland, Liechtenstein and Norway). However, Open Sats commits to process Personal data transferred from the EU to the United States in accordance with the principles of Notice, Choice, Accountability for Onward Transfer, Security, Data Integrity and Purpose Limitation, Access, and Recourse, Enforcement and Liability, as described below.
**International Transfers of Personal Data**. Because MAGIC Grants is a non-profit organization that is not subject to the jurisdiction of the United States Federal Trade Commission, it is not eligible for certification under the EU-U.S. and Swiss-U.S. Privacy Shield frameworks (**Frameworks**) as set forth by the U.S. Department of Commerce regarding the processing of personal data transferred from the EU, United Kingdom, and Switzerland to the U.S. (for these purposes, reference to the EU also includes the European Economic Area countries of Iceland, Liechtenstein and Norway). However, MAGIC Grants commits to process Personal data transferred from the EU to the United States in accordance with the principles of Notice, Choice, Accountability for Onward Transfer, Security, Data Integrity and Purpose Limitation, Access, and Recourse, Enforcement and Liability, as described below.
**Notice and Choice**. This Privacy Policy provides notice of the personal data collected and transferred to the United States and the choices that you have with respect to such personal data. It also provides information about the protections applicable to transferred data below.
**Accountability for Onward Transfers**. We may be accountable for the personal data transfer to third-party service providers. If we are and our third-party service providers process personal data in a manner inconsistent with these principles, we are responsible and liable for the harm caused, unless we prove that we are not responsible for the event giving rise to the damage.
**Security**. We maintain security measures to protect personal data as described in the &quot;Data Security&quot; section of this Privacy Policy.
**Security**. We maintain security measures to protect personal data as described in the **Data Security** section of this Privacy Policy.
**Data Integrity and Purpose Limitation**. We take reasonable steps to ensure that personal data is reliable for its intended use, and that it is accurate, complete and current for as long as we retain it. We will retain the data as long as necessary for the following purposes: delivering the Services, engaging in customer service, complying with legal obligations, auditing, performing security and fraud prevention, responding to legal and regulatory inquiries, and preserving or defending our legal rights or those of other users or third parties.
**Access**. EU users have certain rights to access, correct, amend, or delete personal data where it is inaccurate, or has been processed in violation of these principles. Please see the &quot;Your Rights&quot; section above for more information on the rights of users in the EU (and, to the extent applicable, users in Switzerland).
**Access**. EU users have certain rights to access, correct, amend, or delete personal data where it is inaccurate, or has been processed in violation of these principles. Please see the **Your Rights** section above for more information on the rights of users in the EU (and, to the extent applicable, users in Switzerland).
**Recourse, Enforcement, Liability**. Open Sats commits to resolve complaints about our processing of your personal data. European Union, United Kingdom, and Swiss users with inquiries or complaints regarding our processing of Personal data should first contact Open Sats as follows:
**Recourse, Enforcement, Liability**. MAGIC Grants commits to resolve complaints about our processing of your personal data. European Union, United Kingdom, and Swiss users with inquiries or complaints regarding our processing of Personal data should first contact MAGIC Grants as follows:
Email: info@opensats.org
Email: info@magicgrants.org
Attention: Information Security
We will respond to such inquiries or complaints within thirty (30) days.
**Children&#39;s Privacy**
**Children's Privacy**
Open Sats does not knowingly collect or solicit personal information from anyone under the age of sixteen (16), or knowingly allow such persons to register. If we become aware that we have collected personal information from a child under the relevant age without parental consent, we take steps to delete that information. Where we specifically indicate that we collect personal information from children under sixteen (16), we will obtain the parent or guardian&#39;s consent and provide adequate notice.
MAGIC Grants does not knowingly collect or solicit personal information from anyone under the age of sixteen (16), or knowingly allow such persons to register. If we become aware that we have collected personal information from a child under the relevant age without parental consent, we take steps to delete that information. Where we specifically indicate that we collect personal information from children under sixteen (16), we will obtain the parent or guardian's consent and provide adequate notice.
**Links to Third Party Sites and Services**
@@ -173,14 +146,12 @@ The Sites may contain links to third party sites or online services. Please refe
**California Privacy Rights**
Only to the extent that Open Sats meets the minimum thresholds as required under California law to be subject to the California Privacy Rights Act of 2020, residents of California will be able to request and obtain from us once a year, free of charge, a list of the third parties to whom we have disclosed their personal information (if any) for their direct marketing purposes in the prior calendar year, as well as the types of personal information disclosed to those parties. If you are a California resident and would like to request this information, please submit your request in an email to [info@opensats.org](mailto:info@opensats.org). We reserve the right to ask for verification of your California residence and deny this request if Open Sats believes it is not subject to this requirement.
Only to the extent that MAGIC Grants meets the minimum thresholds as required under California law to be subject to the California Privacy Rights Act of 2020, residents of California will be able to request and obtain from us once a year, free of charge, a list of the third parties to whom we have disclosed their personal information (if any) for their direct marketing purposes in the prior calendar year, as well as the types of personal information disclosed to those parties. If you are a California resident and would like to request this information, please submit your request in an email to [info@magicgrants.org](mailto:info@magicgrants.org). We reserve the right to ask for verification of your California residence and deny this request if MAGIC Grants believes it is not subject to this requirement.
**Contact Us**
If you have any questions about our practices or this Privacy Policy, please contact us at [info@opensats.org](mailto:info@opensats.org), or write to us at: Open Sats Initiative, Attn: Legal Department, 3605 Glenwood Ave., Suite 500, Raleigh, North Carolina, 27612, USA.
If you have any questions about our practices or this Privacy Policy, please contact us at [info@magicgrants.org](mailto:info@magicgrants.org).
**Changes to the Privacy Policy**
This Policy is current as of the effective date set forth above. If we change our privacy policies and procedures, we will post those changes on this page and/or continue to provide access to a copy of the prior version. If we make any changes to this Privacy Policy that materially change how we treat your personal information, we will endeavor to provide you with reasonable notice of such changes, such as via prominent notice on our Sites or to your email address of record, and where required by law, we will obtain your consent or give you the opportunity to opt out of such changes.
3564707v1.AWB.31306.G51883

View File

@@ -0,0 +1,7 @@
# About the MAGIC Privacy Guides Fund
Privacy Guides is a socially motivated website that provides information for protecting your data security and privacy. We are a non-profit project with a mission to inform the public about the value of digital privacy, and about global government initiatives which aim to monitor your online activity. Our website is free of advertisements and not affiliated with any of the listed providers.
Privacy Guides is fiscally hosted by MAGIC Grants, a 501(c)(3) public charity.
More information: [https://www.privacyguides.org/en/about/](https://www.privacyguides.org/en/about/)

View File

View File

@@ -1,97 +1,77 @@
# Open Sats Initiative terms of use
# MAGIC Grants Terms of Use
**Last updated: November 14, 2021**
**Last updated: October 14, 2024**
Welcome to the Opensats.org website and/or mobile application (individually and collectively, the &quot;Site&quot;). Your access and use of the Site and of features, software, products and services provided by Open Sats Initiative (&quot;Open Sats,&quot; &quot;we,&quot; &quot;us,&quot; or &quot;our&quot;) through the Site, (individually and collectively, the &quot;Service&quot;) is subject to the terms and conditions in this Terms of Use (&quot;Terms of Use&quot; or &quot;Terms&quot;).
Welcome to the donate.magicgrants.org website and/or mobile application (individually and collectively, the **Site**). Your access and use of the Site and of features, software, products and services provided by MAGIC Grants (**MAGIC Grants**, **we**, **us**, or **our**) through the Site, (individually and collectively, the **Service**) is subject to the terms and conditions in this Terms of Use (**Terms of Use** or **Terms**).
You must agree to the Terms in order to use the Site and/or the Service. If you use the Site and/or the Service, or click to accept or agree to the Terms if presented to you in a user interface for the Service, this is your acceptance of the Terms and your agreement to all of its terms and conditions. By accepting the Terms or using the Site and/or the Service, you represent and warrant that you are at least 18 years of age and have the legal capacity to enter a contract in the jurisdiction where you reside. If you do not accept the Terms, then you may not use the Site or the Service. If you are using the Site or the Service on behalf of another entity, you represent and warrant that you have full legal authority to bind such other entity to the Terms. If you do not have such authority, then you may not use the Site or the Service and you must discontinue all use of the Site and the Service immediately.
1. Our Services. The purpose of the Service is to promote the use of open source software to enhance blockchain technology and to promote the development of the Bitcoin infrastructure along with other free and open source software projects. As a non-profit organization ourselves, Open Sats Initiative does accept donations on the opensats.org website.
2. Privacy Policy. By clicking on the Terms of Use and Privacy Policy box or using the Site or the Service, you represent that you have read and consent to our Privacy Policy located at [/privacy](/privacy) in addition to the Terms. Open Sats may revise the Privacy Policy at any time, and the new versions will be available on the Site. If at any point you do not agree to any portion of the Privacy Policy, you must immediately stop using the Site and/or Service.
3. Terms of Use Updates. Open Sats may update the Terms at any time, and Open Sats will post the updated version of the Terms on the Site. You understand and agree that you will be deemed to have accepted the updated Terms if you use the Site or the Service after the updated Terms are posted on the Site. If at any point you do not agree to any portion of the Terms then in effect, you must immediately stop using the Site and the Service.
4. Provision of the Services. You understand that all information, data, text, software, graphics or other materials (&quot;Content&quot;), whether publicly posted or privately transmitted, are the sole responsibility of the person from whom such Content originated. This means that you, and not Open Sats, is/are entirely responsible for all Content that you upload, post, email, transmit or otherwise make available via Services. Open Sats does not control the Content posted and, as such, does not guarantee the accuracy, integrity or quality of such Content. Under no circumstances will Open Sats be liable in any way for any Content, including, but not limited to, any errors or omissions in any Content, or any loss or damage of any kind incurred as a result of the use of any Content posted, emailed, transmitted or otherwise made available via the Open Sats Service.
1. Our Services. The purpose of the Service is to promote the charitable mission of MAGIC Grants and its fundraising efforts.
2. Privacy Policy. By clicking on the Terms of Use and Privacy Policy box or using the Site or the Service, you represent that you have read and consent to our Privacy Policy located at [/privacy](/privacy) in addition to the Terms. MAGIC Grants may revise the Privacy Policy at any time, and the new versions will be available on the Site. If at any point you do not agree to any portion of the Privacy Policy, you must immediately stop using the Site and/or Service.
3. Terms of Use Updates. MAGIC Grants may update the Terms at any time, and MAGIC Grants will post the updated version of the Terms on the Site. You understand and agree that you will be deemed to have accepted the updated Terms if you use the Site or the Service after the updated Terms are posted on the Site. If at any point you do not agree to any portion of the Terms then in effect, you must immediately stop using the Site and the Service.
4. Provision of the Services. You understand that all information, data, text, software, graphics or other materials (**Content**), whether publicly posted or privately transmitted, are the sole responsibility of the person from whom such Content originated. This means that you, and not MAGIC Grants, is/are entirely responsible for all Content that you upload, post, email, transmit or otherwise make available via Services. MAGIC Grants does not control the Content posted and, as such, does not guarantee the accuracy, integrity or quality of such Content. Under no circumstances will MAGIC Grants be liable in any way for any Content, including, but not limited to, any errors or omissions in any Content, or any loss or damage of any kind incurred as a result of the use of any Content posted, emailed, transmitted or otherwise made available via the MAGIC Grants Service.
You also agree that you will not collect or store personal data about other users in connection with any prohibited conduct and activities. You acknowledge that Open Sats may or may not pre-screen Content, but that Open Sats and its designees shall have the right (but not the obligation) in their sole discretion to pre-screen, refuse, move, or remove any Content that is available via the Services and which violates the Terms of Service. You agree that you must evaluate, and bear all risks associated with, the use of any Content, including any reliance on the accuracy, completeness, or usefulness of such Content. You acknowledge, consent and agree that Open Sats may access, preserve and disclose your account information and Content if required to do so by law or in a good faith belief that such access preservation or disclosure is reasonably necessary to:
You also agree that you will not collect or store personal data about other users in connection with any prohibited conduct and activities. You acknowledge that MAGIC Grants may or may not pre-screen Content, but that MAGIC Grants and its designees shall have the right (but not the obligation) in their sole discretion to pre-screen, refuse, move, or remove any Content that is available via the Services and which violates the Terms of Service. You agree that you must evaluate, and bear all risks associated with, the use of any Content, including any reliance on the accuracy, completeness, or usefulness of such Content. You acknowledge, consent and agree that MAGIC Grants may access, preserve and disclose your account information and Content if required to do so by law or in a good faith belief that such access preservation or disclosure is reasonably necessary to:
- comply with legal process;
- enforce the Terms of Use and the Privacy Policy;
- respond to claims that any Content violates the rights of third parties;
- respond to your requests for customer service; or
- protect the rights, property or personal safety of Open Sats its users and the public.
- protect the rights, property or personal safety of MAGIC Grants its users and the public.
1. Provision of the Service Not an Exchange. Open Sats is not a bitcoin exchange or wallet service and does not receive, hold or exchange digital currencies on behalf of any other person or entity. Open Sats does not withhold funds for tax purposes or otherwise.
2. Tax Advice. Open Sats makes no representation as to whether all or any portion of your donations to us are tax deductible or eligible for tax credits. Open Sats will have no liability for any claim by any federal, state, local or any other tax authority with respect to the characterization on any applicable tax return of any donation by you. You should consult your tax advisor as to the amount of your donation that is tax deductible or eligible for tax recognition, if any.
3. Access Restriction. Open Sats reserves the right to deny access or service to any person or entity at Open Sats&#39; sole and absolute discretion. You acknowledge and agree that Open Sats may stop providing the Site and/or the Service or restrict your use of the Site and/or the Service at any time, without notifying you in advance, for any reason or no reason, including, without limitation, for any violation of the Terms and/or if Open Sats suspects that you have used any aspect of the Service to conduct any fraudulent or illegal activity. If Open Sats disables your access to your account, you may be prevented from accessing the Service, your account details and/or any materials contained in your account.
4. Accounts and Security.
5. Account. Open Sats may require registration for an account to access the Site and the Services. There will be a registration process for the creation of accounts to complete the registration process. You may be required to provide information about yourself or your company as part of the registration process or your continued use of the Service. You agree that any registration information that you submit to Open Sats will be correct, accurate, and up to date and that you will not impersonate any person or entity, or falsely state or otherwise misrepresent your affiliation with a person or entity.
6. Account Security. Maintaining account security is very important. You are solely responsible for maintaining the confidentiality of your account password. You agree to notify Open Sats immediately if you become aware of any unauthorized use of your password or of your account.
7. Account Sharing or Transfers. Accounts are registered to individuals or entities and may not be sold, traded, gifted or otherwise transferred at any time under any circumstances. An account may not be shared. Passwords may not be disclosed to anyone else.
8. Cancellation by You. You have the right to cancel your account at any time. You may cancel your account by emailing us at info@opensats.org to submit a request to our support team.
9. Termination by Open Sats. Open Sats may at any time terminate your account for any reason or no reason including, without limitation, if:
* Provision of the Service Not an Exchange. MAGIC Grants is not a bitcoin exchange or wallet service and does not receive, hold or exchange digital currencies on behalf of any other person or entity. MAGIC Grants does not withhold funds for tax purposes or otherwise.
* Tax Advice. MAGIC Grants makes no representation as to whether all or any portion of your donations to us are tax deductible or eligible for tax credits. MAGIC Grants will have no liability for any claim by any federal, state, local or any other tax authority with respect to the characterization on any applicable tax return of any donation by you. You should consult your tax advisor as to the amount of your donation that is tax deductible or eligible for tax recognition, if any.
* Access Restriction. MAGIC Grants reserves the right to deny access or service to any person or entity at MAGIC Grants's sole and absolute discretion. You acknowledge and agree that MAGIC Grants may stop providing the Site and/or the Service or restrict your use of the Site and/or the Service at any time, without notifying you in advance, for any reason or no reason, including, without limitation, for any violation of the Terms and/or if MAGIC Grants suspects that you have used any aspect of the Service to conduct any fraudulent or illegal activity. If MAGIC Grants disables your access to your account, you may be prevented from accessing the Service, your account details and/or any materials contained in your account.
- Open Sats determines that you are (a) in breach of or otherwise acting inconsistently with the Terms or written agreements (b) engaging in fraudulent or illegal activities or other conduct that may result in liability to Open Sats;
- Open Sats determines it is required by law to terminate your account; or
- Open Sats decides to stop providing the Service or critical portions of the Service in the country where you reside, access the Site or use the Service or Open Sats determines that it is no longer in its charitable business interests to continue providing the Service or portions of the Service.
## Accounts and Security
1. Content Licensing. Open Sats mission includes educating the public on free and open-source software through content such as articles, images, photographs, comments, software code, audio and video clips, and other materials (collectively, &quot;Content&quot;). Content is authored by Open Sats, contributors to Open Sats projects, and other sources.
* Account. MAGIC Grants may require registration for an account to access the Site and the Services. There will be a registration process for the creation of accounts to complete the registration process. You may be required to provide information about yourself or your company as part of the registration process or your continued use of the Service. You agree that any registration information that you submit to MAGIC Grants will be correct, accurate, and up to date and that you will not impersonate any person or entity, or falsely state or otherwise misrepresent your affiliation with a person or entity.
* *Account Security. Maintaining account security is very important. You are solely responsible for maintaining the confidentiality of your account password. You agree to notify MAGIC Grants immediately if you become aware of any unauthorized use of your password or of your account.
* Account Sharing or Transfers. Accounts are registered to individuals or entities and may not be sold, traded, gifted or otherwise transferred at any time under any circumstances. An account may not be shared. Passwords may not be disclosed to anyone else.
* Cancellation by You. You have the right to cancel your account at any time. You may cancel your account by emailing us at info@magicgrants.org to submit a request to our support team.
* Termination by MAGIC Grants. MAGIC Grants may at any time terminate your account for any reason or no reason including, without limitation, if:
Content authored by Open Sats is generally made available for public sharing and reuse through open licenses such as Creative Commons (for expressive material) or the Open Sats Public License. In most cases we ask Open Sats contributors to release Content under open licenses.
* MAGIC Grants determines that you are (a) in breach of or otherwise acting inconsistently with the Terms or written agreements (b) engaging in fraudulent or illegal activities or other conduct that may result in liability to MAGIC Grants;
* MAGIC Grants determines it is required by law to terminate your account; or
* MAGIC Grants decides to stop providing the Service or critical portions of the Service in the country where you reside, access the Site or use the Service or MAGIC Grants determines that it is no longer in its charitable business interests to continue providing the Service or portions of the Service.
Some Content in our Site may be acquired from sources that prohibit further use of their Content without advance permission. Where possible, the Content will display a notice with the applicable license. You agree to abide by such notices. However it may occur that:
* Effect of Account Termination or Cancellation. If you voluntarily terminate your account or allow your account to lapse, you will be able to reactivate that account at any time through the account interface on the Site. Accounts terminated by MAGIC Grants for any type of abuse including, without limitation, a violation of the Terms, may not be reactivated for any reason. If you are blocked by us from accessing the Site or Service (including by blocking your IP address), you agree not to implement any measures to circumvent such blocking (e.g., by masking your IP address or using a proxy IP address).
1. Some Content expressly indicates that the author does not intend for an open license to apply. You should contact the author or author&#39;s agent for permission to use such Content. Questions on Open Sats authored content can be sent to: info@opensats.org.
2. Some Content contains trademarks, trade dress, logos and brand assets of Open Sats and other parties (&quot;Trademarks&quot;). Except for a few limited circumstances, Trademarks cannot be used without advance written permission of the owner of the Trademark.
3. Software used by our Websites is licensed under the Open Sats Public License or similarly permissive open source licenses.
4. Content Submissions **.** You may contribute Content when interacting with our Site and Services, including but not limited to commenting on an article, blogging, contributing code, or contributing graphics or written material (each a &quot;Submission&quot;). Unless your Submission is made under a separate agreement with Open Sats, in which case that agreement will govern, then
## Restrictions and Conditions of Use
- For Submissions to Open Sats&#39; open source Projects:
* Use of the Site. MAGIC Grants permits users to view and use the Site solely for personal or nonprofit use. You agree not to license, create derivative works from, transfer, sell or re-sell any information, content, materials, data or services obtained from the Site or through the Service unless specifically allowed by an Open Source license or express permission.
* Accessing the Service. You agree not to access, or attempt to access, the Service by any means other than through the user interface provided through the Site or any mobile application provided by MAGIC Grants. You specifically agree not to access, or attempt to access, the Service through any automated means (including, without limitation, through the use of scripts, bots, spiders or web crawlers) and you agree to comply with the instructions contained in any robots.txt file present on the Site or the Service.
* No Violation of Laws. You agree that you will not, in connection with your use of the Site or the Service, violate any applicable law, ordinance, rule, regulation or treaty. Without limiting the foregoing, you agree that you will not make available through the Site and/or the Service any material or information that infringes any copyright, trademark, patent, trade secret, or other right of any party (including rights of privacy or publicity).
* Use Restrictions. You may not connect to or use the Site or the Service in any way that is not expressly permitted by the Terms.
* You may not: (a) remove any proprietary notices from the Service; (b) cause, permit or authorize the modification, creation of derivative works, translation, reverse engineering, decompiling, disassembling or hacking of the Site or the Service; (c) sell, assign, rent, lease, act as a service bureau, or grant rights in the Service, including, without limitation, through sublicense, to any other person or entity without the prior written consent of MAGIC Grants; or (d) make any false, misleading or deceptive statement or representation regarding MAGIC Grants and/or the Site or the Service.
* Without limiting the foregoing, you agree that you will not: (i) institute, assist, or become involved in any type of attack including, without limitation, denial of service attacks, upon the Site and/or the Service (or any servers, systems, or networks connected to the Site or the Service) or otherwise attempt to obstruct, disrupt, or interfere with the operation of the Site and/or the Service or any other person's or entity's use of the Site and/or the Service (or any servers, systems or networks connected to the Site or the Service); (ii) attempt to gain unauthorized access to the Site, the Service, accounts registered to other users, or any servers, systems or networks connected to the Site and/or the Service; (iii) use the Site or the Service for any commercial purpose or the benefit of any third party, or charge any person or entity, or receive any compensation for, the use of the Site or the Service, unless you are specifically authorized to do so in a separate written agreement with MAGIC Grants; (iv) use the Site or the Service to (y) develop, generate, transmit or store information that is defamatory, harmful, abusive, obscene or hateful; or (z) perform any unsolicited commercial communication; or (v) engage in any activity that (A) constitutes harassment or a violation of privacy or threatens other people or groups of people; (B) is harmful to children in any manner; (C) constitutes phishing, pharming, or impersonates any other person or entity, or steals or assumes any person's identity (whether a real identity or online nickname or alias); (D) violates any applicable law, ordinance, rule, regulation or treaty or (E) in MAGIC Grants's sole judgment, is objectionable or that restricts or inhibits any other person from using or enjoying our Site or Service, or which may expose us or our users to any harm or liability of any type.
* No Data Mining or Harmful Code. You agree that you will not (a) obtain or attempt to obtain any information from the Site or Services including, without limitation, any personal information of other account holders or other software data; (b) intercept, examine or otherwise observe any proprietary communications protocol used by the Site or the Service, whether through the use of a network analyzer, packet sniffer, or other device; or (c) use any type of bot, spider, virus, clock, timer, counter, worm, software lock, drop dead device, Trojan horse routing, trap door, time bomb or any other codes, instructions or third party software that is designed to provide a means of surreptitious or unauthorized access to, or distort, delete, damage or disassemble, the Site or the Service.
* Violation of the Terms. You acknowledge and agree that you are solely responsible, and MAGIC Grants has no responsibility or liability to you or any other person or entity, for any breach by you of the Terms or for the consequences of any such breach.
1. You agree to license your Submission under the terms of the corresponding license of the particular open source project to which you are contributing. For more information on the specific license, please see the applicable source.
## Links
- For all other Submissions, you agree to the following in connection with each:
* Links from the Site. The Site may contain links to websites operated by other parties. MAGIC Grants provides these links to other websites as a convenience and use of these websites is at your own risk. The linked websites are not under the control of MAGIC Grants and MAGIC Grants is not responsible for the content available on the other websites. Such links do not imply MAGIC Grants's endorsement of information or material on any other website and MAGIC Grants disclaims all liability with regard to your access to and use of such linked websites.
* Links to the Site. Unless otherwise set forth in a written agreement between you and MAGIC Grants, you must adhere to MAGIC Grants's linking policy as follows: (i) the appearance, position, and other aspects of the link may not be such as to damage or dilute the goodwill associated with MAGIC Grants's and/or its licensors's names and trademarks; (ii) the appearance, position and other attributes of the link may not create the false appearance that your organization or entity is sponsored by, affiliated with, or associated with MAGIC Grants; and (iii) when selected by a user, the link must display the Site on full-screen and not within a **frame** on the linking Site. MAGIC Grants reserves the right to revoke its consent to the link at any time and in its sole discretion.
1. You represent and warrant that your Submission will comply with these Terms and any additional terms that may govern your Submission.
2. You hereby grant us a nonexclusive, royalty-free, worldwide, sublicensable (to those we work with) license to use your Submission in connection with the Content, Site, and Services and online and offline promotion of Open Sats&#39; mission, products and services.
3. You acknowledge that your Submissions may be accessible by other registered users of the applicable service or the public.
4. If your Submission contains expressive material or software code, you agree to license it in a manner that is compatible with the particular website you are making a Submission to.
5. You represent and warrant that you have the rights necessary to grant the rights granted herein, and further, that the uses contemplated under these Terms will not infringe the proprietary or intellectual property rights of any third party.
6. You understand and agree that Open Sats reserves the right, at its discretion, to review, modify, or remove any Submission that it deems is objectionable or in violation of these Terms.
## Intellectual Property
1. Effect of Account Termination or Cancellation. If you voluntarily terminate your account or allow your account to lapse, you will be able to reactivate that account at any time through the account interface on the Site. Accounts terminated by Open Sats for any type of abuse including, without limitation, a violation of the Terms, may not be reactivated for any reason. If you are blocked by us from accessing the Site or Service (including by blocking your IP address), you agree not to implement any measures to circumvent such blocking (e.g., by masking your IP address or using a proxy IP address).
2. Restrictions and Conditions of Use.
3. Use of the Site. Open Sats permits users to view and use the Site solely for personal or nonprofit use. You agree not to license, create derivative works from, transfer, sell or re-sell any information, content, materials, data or services obtained from the Site or through the Service unless specifically allowed by an Open Source license or express permission.
4. Accessing the Service. You agree not to access, or attempt to access, the Service by any means other than through the user interface provided through the Site or any mobile application provided by Open Sats. You specifically agree not to access, or attempt to access, the Service through any automated means (including, without limitation, through the use of scripts, bots, spiders or web crawlers) and you agree to comply with the instructions contained in any robots.txt file present on the Site or the Service.
5. No Violation of Laws. You agree that you will not, in connection with your use of the Site or the Service, violate any applicable law, ordinance, rule, regulation or treaty. Without limiting the foregoing, you agree that you will not make available through the Site and/or the Service any material or information that infringes any copyright, trademark, patent, trade secret, or other right of any party (including rights of privacy or publicity).
6. Use Restrictions. You may not connect to or use the Site or the Service in any way that is not expressly permitted by the Terms.
7. You may not: (a) remove any proprietary notices from the Service; (b) cause, permit or authorize the modification, creation of derivative works, translation, reverse engineering, decompiling, disassembling or hacking of the Site or the Service; (c) sell, assign, rent, lease, act as a service bureau, or grant rights in the Service, including, without limitation, through sublicense, to any other person or entity without the prior written consent of Open Sats; or (d) make any false, misleading or deceptive statement or representation regarding Open Sats and/or the Site or the Service.
8. Without limiting the foregoing, you agree that you will not: (i) institute, assist, or become involved in any type of attack including, without limitation, denial of service attacks, upon the Site and/or the Service (or any servers, systems, or networks connected to the Site or the Service) or otherwise attempt to obstruct, disrupt, or interfere with the operation of the Site and/or the Service or any other person&#39;s or entity&#39;s use of the Site and/or the Service (or any servers, systems or networks connected to the Site or the Service); (ii) attempt to gain unauthorized access to the Site, the Service, accounts registered to other users, or any servers, systems or networks connected to the Site and/or the Service; (iii) use the Site or the Service for any commercial purpose or the benefit of any third party, or charge any person or entity, or receive any compensation for, the use of the Site or the Service, unless you are specifically authorized to do so in a separate written agreement with Open Sats; (iv) use the Site or the Service to (y) develop, generate, transmit or store information that is defamatory, harmful, abusive, obscene or hateful; or (z) perform any unsolicited commercial communication; or (v) engage in any activity that (A) constitutes harassment or a violation of privacy or threatens other people or groups of people; (B) is harmful to children in any manner; (C) constitutes phishing, pharming, or impersonates any other person or entity, or steals or assumes any person&#39;s identity (whether a real identity or online nickname or alias); (D) violates any applicable law, ordinance, rule, regulation or treaty or (E) in Open Sats&#39; sole judgment, is objectionable or that restricts or inhibits any other person from using or enjoying our Site or Service, or which may expose us or our users to any harm or liability of any type.
9. No Data Mining or Harmful Code. You agree that you will not (a) obtain or attempt to obtain any information from the Site or Services including, without limitation, any personal information of other account holders or other software data; (b) intercept, examine or otherwise observe any proprietary communications protocol used by the Site or the Service, whether through the use of a network analyzer, packet sniffer, or other device; or (c) use any type of bot, spider, virus, clock, timer, counter, worm, software lock, drop dead device, Trojan horse routing, trap door, time bomb or any other codes, instructions or third party software that is designed to provide a means of surreptitious or unauthorized access to, or distort, delete, damage or disassemble, the Site or the Service.
10. Violation of the Terms. You acknowledge and agree that you are solely responsible, and Open Sats has no responsibility or liability to you or any other person or entity, for any breach by you of the Terms or for the consequences of any such breach.
11. Links.
12. Links from the Site. The Site may contain links to websites operated by other parties. Open Sats provides these links to other websites as a convenience and use of these websites is at your own risk. The linked websites are not under the control of Open Sats and Open Sats is not responsible for the content available on the other websites. Such links do not imply Open Sats&#39; endorsement of information or material on any other website and Open Sats disclaims all liability with regard to your access to and use of such linked websites.
13. Links to the Site. Unless otherwise set forth in a written agreement between you and Open Sats, you must adhere to Open Sats&#39; linking policy as follows: (i) the appearance, position, and other aspects of the link may not be such as to damage or dilute the goodwill associated with Open Sats&#39; and/or its licensors&#39; names and trademarks; (ii) the appearance, position and other attributes of the link may not create the false appearance that your organization or entity is sponsored by, affiliated with, or associated with Open Sats; and (iii) when selected by a user, the link must display the Site on full-screen and not within a &quot;frame&quot; on the linking Site. Open Sats reserves the right to revoke its consent to the link at any time and in its sole discretion.
14. Intellectual Property.
15. Trademarks. The Open Sats marks are trademarks of Open Sats. Unless permitted in a separate written agreement with Open Sats, you do not have the right to use any of Open Sats&#39; trademarks, service marks, or logos; and your unauthorized use of any of these may be a violation of federal and state trademark laws.
16. Ownership. You acknowledge and agree that Open Sats, or its licensors, owns all right, title and interest in and to the Site and the Service, including all intellectual property, industrial property and proprietary rights recognized anywhere in the world at any time and that the Site and the Service are protected by U.S. and international copyright laws. Further, you acknowledge that the Service may contain information that Open Sats has designated as confidential and you agree not to disclose such information without Open Sats&#39; prior written consent.
* Trademarks. The MAGIC Grants marks are trademarks of MAGIC Grants. Unless permitted in a separate written agreement with MAGIC Grants, you do not have the right to use any of MAGIC Grants's trademarks, service marks, or logos; and your unauthorized use of any of these may be a violation of federal and state trademark laws.
* Ownership. You acknowledge and agree that MAGIC Grants, or its licensors, owns all right, title and interest in and to the Site and the Service, including all intellectual property, industrial property and proprietary rights recognized anywhere in the world at any time and that the Site and the Service are protected by U.S. and international copyright laws. Further, you acknowledge that the Service may contain information that MAGIC Grants has designated as confidential and you agree not to disclose such information without MAGIC Grants's prior written consent.
17. Location. The Site and the Service are operated by Open Sats in the United States. If you choose to access the Site and/or the Service from a location outside of the United States, you do so on your own initiative and you are responsible for compliance with applicable local laws.
## Miscellaneous
18. Children. The Site and the Service are not directed toward children under 13 years of age nor does Open Sats knowingly collect information from children under 13 or allow them to create an account or access account features. If you are under 13, please do not submit any personal information about yourself to Open Sats.
19. Term/Termination. These Terms will continue to apply until ended by either you or Open Sats. You can choose to end them at any time for any reason by discontinuing your use of our Site and Services and, if applicable, deleting your account. We may suspend or terminate your access to our Site and Services at any time for any reason, including, but not limited to, if we reasonably believe: (i) you have violated these Terms, our Privacy Policy, or other relevant policy; (ii) you create risk or possible legal exposure for us; or (iii) our provision of the Site and Services to you is no longer commercially viable. In all such cases, these Terms shall terminate, except that the following sections shall continue to apply: Indemnification, Disclaimer; Limitation of Liability, Modification, and Miscellaneous.
20. Indemnification **.** You agree to defend, indemnify and hold harmless Open Sats, its contractors, contributors, licensors, and partners; and the respective directors, officers, employees and agents of the foregoing (&quot;Indemnified Parties&quot;) from and against any and all third party claims and expenses, including attorneys&#39; fees, arising out of or related to your use of our Site and Services (including, but not limited to, from your Submissions or from your violation of any these Terms).
21. Disclaimer; Limitation of Liability. THE SITE AND SERVICES ARE PROVIDED &quot;AS IS&quot; WITH ALL FAULTS. TO THE EXTENT PERMITTED BY LAW, OPEN SATS AND THE INDEMNIFIED PARTIES HEREBY DISCLAIM ALL WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES THAT THE CONTENT, SITE, AND SERVICES (AS APPLICABLE) ARE FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE, AND NON-INFRINGING. YOU BEAR THE ENTIRE RISK AS TO USING THE SITE AND SERVICES FOR YOUR PURPOSES AND AS TO THE QUALITY AND PERFORMANCE OF THE SITE AND SERVICES, INCLUDING WITHOUT LIMITATION THE RISK THAT YOUR HARDWARE, SOFTWARE, OR CONTENT IS DELETED OR CORRUPTED, THAT SOMEONE ELSE GAINS UNAUTHORIZED ACCESS TO YOUR INFORMATION, OR THAT ANOTHER USER MISUSES OR MISAPPROPRIATES YOUR SUBMISSION. THIS LIMITATION WILL APPLY NOTWITHSTANDING THE FAILURE OF ESSENTIAL PURPOSE OF ANY REMEDY. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF IMPLIED WARRANTIES, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. EXCEPT AS REQUIRED BY LAW, OPEN SATS AND THE INDEMNIFIED PARTIES WILL NOT BE LIABLE FOR ANY INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES ARISING OUT OF OR IN ANY WAY RELATING TO THESE TERMS OR THE USE OF OR INABILITY TO USE THE CONTENT, SITE, AND SERVICES, INCLUDING WITHOUT LIMITATION DIRECT AND INDIRECT DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, LOST PROFITS, LOSS OF DATA, AND COMPUTER FAILURE OR MALFUNCTION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND REGARDLESS OF THE THEORY (CONTRACT, TORT, OR OTHERWISE) UPON WHICH SUCH CLAIM IS BASED. THE COLLECTIVE LIABILITY OF OPEN SATS AND THE INDEMNIFIED PARTIES UNDER THIS AGREEMENT WILL NOT EXCEED \$500 (FIVE HUNDRED DOLLARS). SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL, CONSEQUENTIAL, OR SPECIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
22. Modifications to these Terms **.** We may update these Terms from time to time to address a new feature of the Site and Services or to clarify a provision. The updated Terms will be posted online. If the changes are substantive, we will announce the update through our usual channels for such announcements such as blog posts, banners, emails, or forums. Your continued use of our Site and Services after the effective date of such changes constitutes your acceptance of such changes. To make your review more convenient, we will post an effective date at the top of this page.
23. Miscellaneous. These Terms constitute the entire agreement between you and Open Sats concerning our Site and Services and supersede any prior versions of these Terms. The Site and Services and these Terms are governed by the laws of the state of North Carolina, U.S.A., excluding its conflict of law provisions. All claims and disputes arising out of the Site and Services or these Terms shall be brought exclusively in the courts of Wake County, North Carolina, and you consent to personal jurisdiction in those courts. If any portion of these Terms is held to be invalid or unenforceable, the remaining portions will remain in full force and effect. In the event of a conflict between a translated version of these Terms and the English language version, the English language version shall control. In the event of a conflict between these Terms and relevant additional terms, the additional terms shall control.
24. Contact Open Sats.
* Location. The Site and the Service are operated by MAGIC Grants in the United States. If you choose to access the Site and/or the Service from a location outside of the United States, you do so on your own initiative and you are responsible for compliance with applicable local laws.
* Children. The Site and the Service are not directed toward children under 13 years of age nor does MAGIC Grants knowingly collect information from children under 13 or allow them to create an account or access account features. If you are under 13, please do not submit any personal information about yourself to MAGIC Grants.
* Term/Termination. These Terms will continue to apply until ended by either you or MAGIC Grants. You can choose to end them at any time for any reason by discontinuing your use of our Site and Services and, if applicable, deleting your account. We may suspend or terminate your access to our Site and Services at any time for any reason, including, but not limited to, if we reasonably believe: (i) you have violated these Terms, our Privacy Policy, or other relevant policy; (ii) you create risk or possible legal exposure for us; or (iii) our provision of the Site and Services to you is no longer commercially viable. In all such cases, these Terms shall terminate, except that the following sections shall continue to apply: Indemnification, Disclaimer; Limitation of Liability, Modification, and Miscellaneous.
* Indemnification. You agree to defend, indemnify and hold harmless MAGIC Grants, its contractors, contributors, licensors, and partners; and the respective directors, officers, employees and agents of the foregoing (**Indemnified Parties**) from and against any and all third party claims and expenses, including attorneys' fees, arising out of or related to your use of our Site and Services (including, but not limited to, from your Submissions or from your violation of any these Terms).
21. Disclaimer; Limitation of Liability. THE SITE AND SERVICES ARE PROVIDED "AS IS" WITH ALL FAULTS. TO THE EXTENT PERMITTED BY LAW, MAGIC Grants AND THE INDEMNIFIED PARTIES HEREBY DISCLAIM ALL WARRANTIES, WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES THAT THE CONTENT, SITE, AND SERVICES (AS APPLICABLE) ARE FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE, AND NON-INFRINGING. YOU BEAR THE ENTIRE RISK AS TO USING THE SITE AND SERVICES FOR YOUR PURPOSES AND AS TO THE QUALITY AND PERFORMANCE OF THE SITE AND SERVICES, INCLUDING WITHOUT LIMITATION THE RISK THAT YOUR HARDWARE, SOFTWARE, OR CONTENT IS DELETED OR CORRUPTED, THAT SOMEONE ELSE GAINS UNAUTHORIZED ACCESS TO YOUR INFORMATION, OR THAT ANOTHER USER MISUSES OR MISAPPROPRIATES YOUR SUBMISSION. THIS LIMITATION WILL APPLY NOTWITHSTANDING THE FAILURE OF ESSENTIAL PURPOSE OF ANY REMEDY. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF IMPLIED WARRANTIES, SO THIS DISCLAIMER MAY NOT APPLY TO YOU. EXCEPT AS REQUIRED BY LAW, MAGIC Grants AND THE INDEMNIFIED PARTIES WILL NOT BE LIABLE FOR ANY INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES ARISING OUT OF OR IN ANY WAY RELATING TO THESE TERMS OR THE USE OF OR INABILITY TO USE THE CONTENT, SITE, AND SERVICES, INCLUDING WITHOUT LIMITATION DIRECT AND INDIRECT DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, LOST PROFITS, LOSS OF DATA, AND COMPUTER FAILURE OR MALFUNCTION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES AND REGARDLESS OF THE THEORY (CONTRACT, TORT, OR OTHERWISE) UPON WHICH SUCH CLAIM IS BASED. THE COLLECTIVE LIABILITY OF MAGIC Grants AND THE INDEMNIFIED PARTIES UNDER THIS AGREEMENT WILL NOT EXCEED \$500 (FIVE HUNDRED DOLLARS). SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL, CONSEQUENTIAL, OR SPECIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
22. Modifications to these Terms. We may update these Terms from time to time to address a new feature of the Site and Services or to clarify a provision. The updated Terms will be posted online. If the changes are substantive, we will announce the update through our usual channels for such announcements such as blog posts, banners, emails, or forums. Your continued use of our Site and Services after the effective date of such changes constitutes your acceptance of such changes. To make your review more convenient, we will post an effective date at the top of this page.
23. Miscellaneous. These Terms constitute the entire agreement between you and MAGIC Grants concerning our Site and Services and supersede any prior versions of these Terms. The Site and Services and these Terms are governed by the laws of the state of Colorado, U.S.A., excluding its conflict of law provisions. All claims and disputes arising out of the Site and Services or these Terms shall be brought exclusively in the courts of Denver County, Colorado, and you consent to personal jurisdiction in those courts. If any portion of these Terms is held to be invalid or unenforceable, the remaining portions will remain in full force and effect. In the event of a conflict between a translated version of these Terms and the English language version, the English language version shall control. In the event of a conflict between these Terms and relevant additional terms, the additional terms shall control.
> Open Sats
## Contact MAGIC Grants.
> MAGIC Grants
>
> Attn: Open Sats Legal Notices
>
> P.O. Box 20389
>
> Raleigh, NC 27612
>
> info@opensats.org
>
> 3566450v1.AWB.31306.G51883
> Attn: MAGIC Grants Legal Notices
>
> info@magicgrants.org

113
env.mjs Normal file
View File

@@ -0,0 +1,113 @@
// 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: {
BUILD_MODE: z.boolean(),
APP_URL: z.string().url(),
NEXTAUTH_SECRET: z.string().min(32),
USER_SETTINGS_JWT_SECRET: z.string().min(32),
SMTP_HOST: z.string().min(1),
SMTP_PORT: z.string().min(1),
SMTP_USER: z.string().min(1),
SMTP_PASS: z.string().min(1),
SES_VERIFIED_SENDER: z.string().email(),
STRIPE_MONERO_SECRET_KEY: z.string().min(1),
STRIPE_MONERO_WEBHOOK_SECRET: z.string().min(1),
STRIPE_FIRO_SECRET_KEY: z.string().min(1),
STRIPE_FIRO_WEBHOOK_SECRET: z.string().min(1),
STRIPE_PRIVACY_GUIDES_SECRET_KEY: z.string().min(1),
STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET: z.string().min(1),
STRIPE_GENERAL_SECRET_KEY: z.string().min(1),
STRIPE_GENERAL_WEBHOOK_SECRET: z.string().min(1),
KEYCLOAK_URL: z.string().url(),
KEYCLOAK_CLIENT_ID: z.string().min(1),
KEYCLOAK_CLIENT_SECRET: z.string().min(1),
KEYCLOAK_REALM_NAME: z.string().min(1),
BTCPAY_URL: z.string().url(),
BTCPAY_EXTERNAL_URL: z.string().url(),
BTCPAY_API_KEY: z.string().min(1),
BTCPAY_STORE_ID: z.string().min(1),
BTCPAY_WEBHOOK_SECRET: z.string().min(1),
MONERO_APPLICATION_RECIPIENT: z.string().email(),
FIRO_APPLICATION_RECIPIENT: z.string().email(),
PRIVACY_GUIDES_APPLICATION_RECIPIENT: z.string().email(),
GENERAL_APPLICATION_RECIPIENT: z.string().email(),
},
/*
* Environment variables available on the client (and server).
*
* 💡 You'll get type errors if these are not prefixed with NEXT_PUBLIC_.
*/
client: {
NEXT_PUBLIC_MONERO_APPLICATION_RECIPIENT: z.string().email(),
NEXT_PUBLIC_FIRO_APPLICATION_RECIPIENT: z.string().email(),
NEXT_PUBLIC_PRIVACY_GUIDES_APPLICATION_RECIPIENT: z.string().email(),
NEXT_PUBLIC_GENERAL_APPLICATION_RECIPIENT: z.string().email(),
},
/*
* 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: {
BUILD_MODE: !!process.env.BUILD_MODE,
APP_URL: process.env.APP_URL,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
USER_SETTINGS_JWT_SECRET: process.env.USER_SETTINGS_JWT_SECRET,
SMTP_HOST: process.env.SMTP_HOST,
SMTP_PORT: process.env.SMTP_PORT,
SMTP_USER: process.env.SMTP_USER,
SMTP_PASS: process.env.SMTP_PASS,
SES_VERIFIED_SENDER: process.env.SES_VERIFIED_SENDER,
STRIPE_MONERO_SECRET_KEY: process.env.STRIPE_MONERO_SECRET_KEY,
STRIPE_MONERO_WEBHOOK_SECRET: process.env.STRIPE_MONERO_WEBHOOK_SECRET,
STRIPE_FIRO_SECRET_KEY: process.env.STRIPE_FIRO_SECRET_KEY,
STRIPE_FIRO_WEBHOOK_SECRET: process.env.STRIPE_FIRO_WEBHOOK_SECRET,
STRIPE_PRIVACY_GUIDES_SECRET_KEY: process.env.STRIPE_PRIVACY_GUIDES_SECRET_KEY,
STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET: process.env.STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET,
STRIPE_GENERAL_SECRET_KEY: process.env.STRIPE_GENERAL_SECRET_KEY,
STRIPE_GENERAL_WEBHOOK_SECRET: process.env.STRIPE_GENERAL_WEBHOOK_SECRET,
KEYCLOAK_URL: process.env.KEYCLOAK_URL,
BTCPAY_EXTERNAL_URL: process.env.BTCPAY_EXTERNAL_URL,
KEYCLOAK_CLIENT_ID: process.env.KEYCLOAK_CLIENT_ID,
KEYCLOAK_CLIENT_SECRET: process.env.KEYCLOAK_CLIENT_SECRET,
KEYCLOAK_REALM_NAME: process.env.KEYCLOAK_REALM_NAME,
BTCPAY_URL: process.env.BTCPAY_URL,
BTCPAY_API_KEY: process.env.BTCPAY_API_KEY,
BTCPAY_STORE_ID: process.env.BTCPAY_STORE_ID,
BTCPAY_WEBHOOK_SECRET: process.env.BTCPAY_WEBHOOK_SECRET,
MONERO_APPLICATION_RECIPIENT: process.env.MONERO_APPLICATION_RECIPIENT,
FIRO_APPLICATION_RECIPIENT: process.env.FIRO_APPLICATION_RECIPIENT,
PRIVACY_GUIDES_APPLICATION_RECIPIENT: process.env.PRIVACY_GUIDES_APPLICATION_RECIPIENT,
GENERAL_APPLICATION_RECIPIENT: process.env.GENERAL_APPLICATION_RECIPIENT,
NEXT_PUBLIC_MONERO_APPLICATION_RECIPIENT: process.env.NEXT_PUBLIC_MONERO_APPLICATION_RECIPIENT,
NEXT_PUBLIC_FIRO_APPLICATION_RECIPIENT: process.env.NEXT_PUBLIC_FIRO_APPLICATION_RECIPIENT,
NEXT_PUBLIC_PRIVACY_GUIDES_APPLICATION_RECIPIENT:
process.env.NEXT_PUBLIC_PRIVACY_GUIDES_APPLICATION_RECIPIENT,
NEXT_PUBLIC_GENERAL_APPLICATION_RECIPIENT:
process.env.NEXT_PUBLIC_GENERAL_APPLICATION_RECIPIENT,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
* useful for Docker builds.
*/
skipValidation: !!process.env.BUILD_MODE,
})

27
middleware.ts Normal file
View File

@@ -0,0 +1,27 @@
import { withAuth } from 'next-auth/middleware'
import { refreshToken } from './server/utils/auth'
export default withAuth({
pages: {
signIn: '/',
},
callbacks: {
async authorized({ token }) {
if (!token) return false
if (Date.now() < token.accessTokenExpiresAt && !token.error) {
return true
}
const newToken = await refreshToken(token)
if (Date.now() < newToken.accessTokenExpiresAt && !newToken.error) {
return true
}
return false
},
},
})
export const config = { matcher: ['/:path/account/:path*'] }

2
next-env.d.ts vendored
View File

@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.

View File

@@ -2,6 +2,14 @@
const nextConfig = {
reactStrictMode: true,
output: 'standalone',
webpack: (config, options) => {
config.module.rules.push({
test: /\.svg$/,
use: ['@svgr/webpack'],
})
return config
},
}
module.exports = nextConfig

32
nginx.conf Normal file
View File

@@ -0,0 +1,32 @@
events {
worker_connections 1024;
}
http {
limit_req_zone $binary_remote_addr zone=api:10m rate=2r/s;
server {
listen 80;
server_name donate.magicgrants.org;
location / {
proxy_pass http://app:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
}
location /api {
proxy_pass http://app:3000/api;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
limit_req zone=api burst=5 nodelay;
}
}
}

13319
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,55 +10,96 @@
"watch": "npm-watch"
},
"dependencies": {
"@fontsource/source-code-pro": "^5.0.18",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@sendgrid/mail": "^8.1.3",
"@hookform/resolvers": "^3.6.0",
"@keycloak/keycloak-admin-client": "^24.0.5",
"@prisma/client": "^5.15.1",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-radio-group": "^1.1.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"@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",
"axios": "^1.7.4",
"base-64": "^1.0.0",
"bootstrap": "^5.3.3",
"class-variance-authority": "^0.7.0",
"classnames": "^2.5.1",
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
"dotenv": "^16.4.5",
"escape-html": "^1.0.3",
"exclude": "^1.0.0",
"fs": "^0.0.1-security",
"gray-matter": "^4.0.3",
"jsonwebtoken": "^9.0.2",
"jwt-decode": "^4.0.0",
"lucide-react": "^0.390.0",
"micro": "^10.0.1",
"micro-cors": "^0.1.1",
"next": "^14.2.3",
"next-auth": "^4.24.7",
"next-themes": "^0.3.0",
"nodemailer": "^6.9.13",
"raw-body": "^2.5.2",
"react": "^18.3.1",
"react-bootstrap": "^2.10.2",
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.5",
"react-modal": "^3.16.1",
"react-social-icons": "^6.18.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark": "^15.0.1",
"remark-gfm": "^4.0.0",
"remark-html": "^16.0.1",
"router": "^1.3.8",
"remark-rehype": "^11.1.1",
"sanitize-filename": "^1.6.3",
"sharp": "^0.33.4",
"stripe": "^15.9.0",
"superjson": "^2.2.1",
"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",
"@tailwindcss/line-clamp": "^0.4.4",
"@tailwindcss/typography": "^0.5.13",
"@types/base-64": "^1.0.2",
"@types/jsonwebtoken": "^9.0.6",
"@types/memory-cache": "^0.2.6",
"@types/node": "20.14.0",
"@types/nodemailer": "^6.4.15",
"@types/react": "18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-modal": "^3.16.3",
"autoprefixer": "^10.4.19",
"eslint": "8.26.0",
"eslint-config-next": "14.2.3",
"eslint": "^8.56.0",
"eslint-config-next": "^14.2.3",
"npm-watch": "^0.13.0",
"postcss": "^8.4.38",
"prettier": "^3.3.0",
"prettier": "^3.3.3",
"prisma": "^5.15.1",
"tailwindcss": "^3.4.3",
"typescript": "5.1.6"
},

36
pages/[fund]/about.tsx Normal file
View File

@@ -0,0 +1,36 @@
import xss from 'xss'
import { FundSlug } from '@prisma/client'
import markdownToHtml from '../../utils/markdownToHtml'
import { fileExists, getSingleFile } from '../../utils/md'
import { fundSlugs } from '../../utils/funds'
export default function About({ content }: { content: string }) {
return (
<article
className="prose max-w-3xl mx-auto pb-8 pt-8 xl:col-span-2"
dangerouslySetInnerHTML={{ __html: xss(content || '') }}
/>
)
}
export async function getStaticProps({ params }: { params: { fund: FundSlug } }) {
const md = getSingleFile(`docs/${params.fund}/about_us.md`)
const content = await markdownToHtml(md || '')
return {
props: {
content,
},
}
}
export function getStaticPaths() {
return {
paths: fundSlugs
.filter((fundSlug) => fileExists(`docs/${fundSlug}/about.md`))
.map((fundSlug) => `/${fundSlug}/about`),
fallback: true,
}
}

View File

@@ -0,0 +1,67 @@
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import Head from 'next/head'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../../../components/ui/table'
import { trpc } from '../../../utils/trpc'
import { useFundSlug } from '../../../utils/use-fund-slug'
dayjs.extend(localizedFormat)
const donationTypePretty = {
one_time: 'One-time',
recurring: 'Recurring',
}
function MyDonations() {
const fundSlug = useFundSlug()
// Conditionally render hooks should be ok in this case
if (!fundSlug) return <></>
const donationListQuery = trpc.donation.donationList.useQuery({ fundSlug })
return (
<>
<Head>
<title>Monero Fund - My Donations</title>
</Head>
<div className="w-full max-w-5xl h-full mx-auto flex flex-col">
<h1 className="text-3xl font-bold mb-4">My Donations</h1>
<div className="w-full flex overflow-x-auto grow">
<Table className="min-w-[700px] grow">
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Method</TableHead>
<TableHead>Amount</TableHead>
<TableHead>Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{donationListQuery.data?.map((donation) => (
<TableRow key={donation.createdAt.toISOString()}>
<TableCell>{donation.projectName}</TableCell>
<TableCell>{donation.btcPayInvoiceId ? 'Crypto' : 'Fiat'}</TableCell>
<TableCell>${donation.grossFiatAmount}</TableCell>
<TableCell>{dayjs(donation.createdAt).format('lll')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</>
)
}
export default MyDonations

View File

@@ -0,0 +1,75 @@
import dayjs from 'dayjs'
import localizedFormat from 'dayjs/plugin/localizedFormat'
import Head from 'next/head'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../../../components/ui/table'
import { trpc } from '../../../utils/trpc'
import CustomLink from '../../../components/CustomLink'
import { useFundSlug } from '../../../utils/use-fund-slug'
dayjs.extend(localizedFormat)
function MyMemberships() {
const fundSlug = useFundSlug()
// Conditionally render hooks should be ok in this case
if (!fundSlug) return <></>
const membershipListQuery = trpc.donation.membershipList.useQuery({ fundSlug })
return (
<>
<Head>
<title>Monero Fund - My Memberships</title>
</Head>
<div className="w-full max-w-5xl h-full mx-auto flex flex-col">
<div className="flex flex-row justify-between">
<h1 className="text-3xl font-bold mb-4">My Memberships</h1>
{membershipListQuery.data?.billingPortalUrl && (
<CustomLink
href={membershipListQuery.data?.billingPortalUrl}
aria-label="Manage Fiat Subscriptions"
>
Manage Recurring Memberships
</CustomLink>
)}
</div>
<div className="w-full flex overflow-x-auto grow">
<Table className="min-w-[700px] grow">
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Method</TableHead>
<TableHead>Recurring</TableHead>
<TableHead>Date</TableHead>
<TableHead>Period End</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{membershipListQuery.data?.memberships.map((membership) => (
<TableRow key={membership.createdAt.toISOString()}>
<TableCell>{membership.projectName}</TableCell>
<TableCell>{membership.btcPayInvoiceId ? 'Crypto' : 'Fiat'}</TableCell>
<TableCell>{membership.stripeSubscriptionId ? 'Yes' : 'No'}</TableCell>
<TableCell>{dayjs(membership.createdAt).format('lll')}</TableCell>
<TableCell>{dayjs(membership.membershipExpiresAt).format('lll')}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</>
)
}
export default MyMemberships

View File

@@ -0,0 +1,220 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import Head from 'next/head'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../../../components/ui/form'
import { Input } from '../../../components/ui/input'
import { useForm } from 'react-hook-form'
import { Button } from '../../../components/ui/button'
import Spinner from '../../../components/Spinner'
import { toast } from '../../../components/ui/use-toast'
import { trpc } from '../../../utils/trpc'
import { useFundSlug } from '../../../utils/use-fund-slug'
import { signOut, useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
const changePasswordFormSchema = z
.object({
currentPassword: z.string().min(8),
newPassword: z.string().min(8),
confirmNewPassword: z.string().min(8),
})
.refine((data) => data.newPassword === data.confirmNewPassword, {
message: 'Passwords do not match.',
path: ['confirmNewPassword'],
})
const changeEmailFormSchema = z.object({ newEmail: z.string().email() })
type ChangePasswordFormInputs = z.infer<typeof changePasswordFormSchema>
type ChangeEmailFormInputs = z.infer<typeof changeEmailFormSchema>
function Settings() {
const fundSlug = useFundSlug()
const session = useSession()
const changePasswordMutation = trpc.account.changePassword.useMutation()
const requestEmailChangeMutation = trpc.account.requestEmailChange.useMutation()
const changePasswordForm = useForm<ChangePasswordFormInputs>({
resolver: zodResolver(changePasswordFormSchema),
defaultValues: {
currentPassword: '',
newPassword: '',
confirmNewPassword: '',
},
mode: 'all',
})
const changeEmailForm = useForm<ChangeEmailFormInputs>({
resolver: zodResolver(changeEmailFormSchema),
defaultValues: { newEmail: '' },
mode: 'all',
})
async function onChangePasswordSubmit(data: ChangePasswordFormInputs) {
try {
await changePasswordMutation.mutateAsync({
currentPassword: data.currentPassword,
newPassword: data.newPassword,
})
changePasswordForm.reset()
toast({ title: 'Password successfully changed! Please log in again.' })
await signOut({ callbackUrl: `/${fundSlug}/?loginEmail=${session.data?.user.email}` })
} catch (error) {
const errorMessage = (error as any).message
if (errorMessage === 'INVALID_PASSWORD') {
return changePasswordForm.setError(
'currentPassword',
{ message: 'Invalid password.' },
{ shouldFocus: true }
)
}
return toast({
title: 'Sorry, something went wrong.',
variant: 'destructive',
})
}
}
async function onChangeEmailSubmit(data: ChangeEmailFormInputs) {
if (!fundSlug) return
try {
await requestEmailChangeMutation.mutateAsync({ fundSlug, newEmail: data.newEmail })
changeEmailForm.reset()
toast({ title: 'A verification link has been sent to your email.' })
} catch (error) {
const errorMessage = (error as any).message
if (errorMessage === 'EMAIL_TAKEN') {
return changeEmailForm.setError(
'newEmail',
{ message: 'Email is already taken.' },
{ shouldFocus: true }
)
}
return toast({
title: 'Sorry, something went wrong.',
variant: 'destructive',
})
}
}
return (
<>
<Head>
<title>MAGIC Grants - Settings</title>
</Head>
<div className="w-full max-w-lg mx-auto flex flex-col space-y-12">
<div className="w-full flex flex-col space-y-6">
<h1 className="font-bold">Change Password</h1>
<Form {...changePasswordForm}>
<form
onSubmit={changePasswordForm.handleSubmit(onChangePasswordSubmit)}
className="flex flex-col space-y-4"
>
<FormField
control={changePasswordForm.control}
name="currentPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Current password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={changePasswordForm.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>New password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={changePasswordForm.control}
name="confirmNewPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm new password</FormLabel>
<FormControl>
<Input type="password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
disabled={
changePasswordForm.formState.isSubmitting || !changePasswordForm.formState.isValid
}
>
{changePasswordForm.formState.isSubmitting && <Spinner />} Change Password
</Button>
</form>
</Form>
</div>
<div className="w-full flex flex-col space-y-6">
<h1 className="font-bold">Change Email</h1>
<Form {...changeEmailForm}>
<form
onSubmit={changeEmailForm.handleSubmit(onChangeEmailSubmit)}
className="flex flex-col space-y-4"
>
<FormField
control={changeEmailForm.control}
name="newEmail"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder={session.data?.user.email} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
disabled={
changeEmailForm.formState.isSubmitting || !changeEmailForm.formState.isValid
}
>
{changeEmailForm.formState.isSubmitting && <Spinner />} Change Email
</Button>
</form>
</Form>
</div>
</div>
</>
)
}
export default Settings

View File

@@ -0,0 +1,31 @@
import { FundSlug } from '@prisma/client'
import markdownToHtml from '../../utils/markdownToHtml'
import { fileExists, getSingleFile } from '../../utils/md'
import BigDumbMarkdown from '../../components/BigDumbMarkdown'
import { fundSlugs } from '../../utils/funds'
import { fundHeaderNavLinks } from '../../data/headerNavLinks'
export default function About({ content }: { content: string }) {
return <BigDumbMarkdown content={content} />
}
export async function getStaticProps({ params }: { params: { fund: FundSlug } }) {
const md = getSingleFile(`docs/${params.fund}/apply_research.md`)
const content = await markdownToHtml(md || '')
return {
props: {
content,
},
}
}
export function getStaticPaths() {
return {
paths: fundSlugs
.filter((fundSlug) => fileExists(`docs/${fundSlug}/apply_research.md`))
.map((fundSlug) => `/${fundSlug}/apply_research`),
fallback: true,
}
}

36
pages/[fund]/faq.tsx Normal file
View File

@@ -0,0 +1,36 @@
import xss from 'xss'
import { FundSlug } from '@prisma/client'
import markdownToHtml from '../../utils/markdownToHtml'
import { fileExists, getSingleFile } from '../../utils/md'
import { fundSlugs } from '../../utils/funds'
export default function Faq({ content }: { content: string }) {
return (
<article
className="prose max-w-3xl mx-auto pb-8 pt-8 xl:col-span-2"
dangerouslySetInnerHTML={{ __html: xss(content || '') }}
/>
)
}
export async function getStaticProps({ params }: { params: { fund: FundSlug } }) {
const md = getSingleFile(`docs/${params.fund}/faq.md`)
const content = await markdownToHtml(md || '')
return {
props: {
content,
},
}
}
export function getStaticPaths() {
return {
paths: fundSlugs
.filter((fundSlug) => fileExists(`docs/${fundSlug}/faq.md`))
.map((fundSlug) => `/${fundSlug}/faq`),
fallback: true,
}
}

View File

@@ -0,0 +1,293 @@
import { useEffect, useState } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { GetServerSidePropsContext, NextPage } from 'next/types'
import Head from 'next/head'
import ErrorPage from 'next/error'
import Image from 'next/image'
import xss from 'xss'
import { ProjectDonationStats, ProjectItem } from '../../../utils/types'
import { getProjectBySlug } from '../../../utils/md'
import markdownToHtml from '../../../utils/markdownToHtml'
import PageHeading from '../../../components/PageHeading'
import Progress from '../../../components/Progress'
import { prisma } from '../../../server/services'
import { Button } from '../../../components/ui/button'
import { Dialog, DialogContent } from '../../../components/ui/dialog'
import DonationFormModal from '../../../components/DonationFormModal'
import MembershipFormModal from '../../../components/MembershipFormModal'
import LoginFormModal from '../../../components/LoginFormModal'
import RegisterFormModal from '../../../components/RegisterFormModal'
import PasswordResetFormModal from '../../../components/PasswordResetFormModal'
import CustomLink from '../../../components/CustomLink'
import { trpc } from '../../../utils/trpc'
import { getFundSlugFromUrlPath } from '../../../utils/funds'
import { useFundSlug } from '../../../utils/use-fund-slug'
type SingleProjectPageProps = {
project: ProjectItem
projects: ProjectItem[]
donationStats: ProjectDonationStats
}
const Project: NextPage<SingleProjectPageProps> = ({ project, donationStats }) => {
const router = useRouter()
const [donateModalOpen, setDonateModalOpen] = useState(false)
const [memberModalOpen, setMemberModalOpen] = useState(false)
const [registerIsOpen, setRegisterIsOpen] = useState(false)
const [loginIsOpen, setLoginIsOpen] = useState(false)
const [passwordResetIsOpen, setPasswordResetIsOpen] = useState(false)
const session = useSession()
const fundSlug = useFundSlug()
const userHasMembershipQuery = trpc.donation.userHasMembership.useQuery(
{ projectSlug: project.slug },
{ enabled: false }
)
const { slug, title, summary, coverImage, content, nym, website, goal, isFunded } = project
function formatBtc(bitcoin: number) {
if (bitcoin > 0.1) {
return `${bitcoin.toFixed(3) || 0.0} BTC`
} else {
return `${Math.floor(bitcoin * 100000000).toLocaleString()} sats`
}
}
function formatUsd(dollars: number): string {
if (dollars == 0) {
return '$0'
} else if (dollars / 1000 > 1) {
return `$${Math.round(dollars / 1000)}k+`
} else {
return `$${dollars.toFixed(0)}`
}
}
useEffect(() => {
if (session.status === 'authenticated') {
userHasMembershipQuery.refetch()
}
}, [session.status])
if (!router.isFallback && !slug) {
return <ErrorPage statusCode={404} />
}
return (
<>
<Head>
<title>Monero Fund | {project.title}</title>
</Head>
<div className="divide-y divide-gray-200">
<PageHeading project={project}>
<div className="w-full mt-8 flex flex-col md:flex-row items-center md:space-x-8 xl:space-x-0 space-y-10 md:space-y-0 xl:block">
<Image
src={coverImage}
alt="avatar"
width={700}
height={700}
className="w-full max-w-[700px] mx-auto object-contain xl:hidden"
/>
<div className="w-full max-w-96 space-y-8 p-6 bg-white rounded-xl">
<div className="w-full">
{!project.isFunded && (
<div className="flex flex-col space-y-2">
<Button onClick={() => setDonateModalOpen(true)}>Donate</Button>
{!userHasMembershipQuery.data && (
<Button
onClick={() =>
session.status === 'authenticated'
? setMemberModalOpen(true)
: setRegisterIsOpen(true)
}
variant="outline"
>
Get Annual Membership
</Button>
)}
{!!userHasMembershipQuery.data && (
<Button variant="outline">
<CustomLink href={`${fundSlug}/account/my-memberships`}>
My Memberships
</CustomLink>
</Button>
)}
</div>
)}
</div>
<div className="w-full">
<h1 className="mb-4 font-bold">Raised</h1>
<Progress
current={
donationStats.xmr.fiatAmount +
donationStats.btc.fiatAmount +
donationStats.usd.fiatAmount
}
goal={goal}
/>
<ul className="font-semibold space-y-1">
<li className="flex items-center space-x-1">
<span className="text-green-500 text-xl">{`${formatUsd(donationStats.xmr.fiatAmount + donationStats.btc.fiatAmount + donationStats.usd.fiatAmount)}`}</span>{' '}
<span className="font-normal text-sm text-gray">
in{' '}
{donationStats.xmr.count + donationStats.btc.count + donationStats.usd.count}{' '}
donations total
</span>
</li>
<li>
{donationStats.xmr.amount} XMR{' '}
<span className="font-normal text-sm text-gray">
in {donationStats.xmr.count} donations
</span>
</li>
<li>
{formatBtc(donationStats.btc.amount)}{' '}
<span className="font-normal text-sm text-gray">
in {donationStats.btc.count} donations
</span>
</li>
<li>
{`${formatUsd(donationStats.usd.amount)}`} Fiat{' '}
<span className="font-normal text-sm text-gray">
in {donationStats.usd.count} donations
</span>
</li>
</ul>
</div>
</div>
</div>
<article
className="prose max-w-none pb-8 pt-8 xl:col-span-2"
dangerouslySetInnerHTML={{ __html: xss(content || '') }}
/>
</PageHeading>
</div>
<Dialog open={donateModalOpen} onOpenChange={setDonateModalOpen}>
<DialogContent>
<DonationFormModal
project={project}
close={() => setDonateModalOpen(false)}
openRegisterModal={() => setRegisterIsOpen(true)}
/>
</DialogContent>
</Dialog>
<Dialog open={memberModalOpen} onOpenChange={setMemberModalOpen}>
<DialogContent>
<MembershipFormModal
project={project}
close={() => setMemberModalOpen(false)}
openRegisterModal={() => setRegisterIsOpen(true)}
/>
</DialogContent>
</Dialog>
{session.status !== 'authenticated' && (
<>
<Dialog open={loginIsOpen} onOpenChange={setLoginIsOpen}>
<DialogContent>
<LoginFormModal
close={() => setLoginIsOpen(false)}
openRegisterModal={() => setRegisterIsOpen(true)}
openPasswordResetModal={() => setPasswordResetIsOpen(true)}
/>
</DialogContent>
</Dialog>
<Dialog open={registerIsOpen} onOpenChange={setRegisterIsOpen}>
<DialogContent>
<RegisterFormModal
openLoginModal={() => setLoginIsOpen(true)}
close={() => setRegisterIsOpen(false)}
/>
</DialogContent>
</Dialog>
<Dialog open={passwordResetIsOpen} onOpenChange={setPasswordResetIsOpen}>
<DialogContent>
<PasswordResetFormModal close={() => setPasswordResetIsOpen(false)} />
</DialogContent>
</Dialog>
</>
)}
</>
)
}
export default Project
export async function getServerSideProps({ params, resolvedUrl }: GetServerSidePropsContext) {
const fundSlug = getFundSlugFromUrlPath(resolvedUrl)
if (!params?.slug) return {}
if (!fundSlug) return {}
const project = getProjectBySlug(params.slug as string, fundSlug)
const content = await markdownToHtml(project.content || '')
const donationStats = {
xmr: {
count: project.isFunded ? project.numDonationsXMR : 0,
amount: project.isFunded ? project.totalDonationsXMR : 0,
fiatAmount: project.isFunded ? project.totalDonationsXMRInFiat : 0,
},
btc: {
count: project.isFunded ? project.numDonationsBTC : 0,
amount: project.isFunded ? project.totalDonationsBTC : 0,
fiatAmount: project.isFunded ? project.totalDonationsBTCInFiat : 0,
},
usd: {
count: project.isFunded ? project.numDonationsFiat : 0,
amount: project.isFunded ? project.totalDonationsFiat : 0,
fiatAmount: project.isFunded ? project.totalDonationsFiat : 0,
},
}
if (!project.isFunded) {
const donations = await prisma.donation.findMany({
where: { projectSlug: params.slug as string, fundSlug },
})
donations.forEach((donation) => {
if (donation.cryptoCode === 'XMR') {
donationStats.xmr.count += 1
donationStats.xmr.amount += donation.netCryptoAmount || 0
donationStats.xmr.fiatAmount += donation.netFiatAmount
}
if (donation.cryptoCode === 'BTC') {
donationStats.btc.count += 1
donationStats.btc.amount += donation.netCryptoAmount || 0
donationStats.btc.fiatAmount += donation.netFiatAmount
}
if (donation.cryptoCode === null) {
donationStats.usd.count += 1
donationStats.usd.amount += donation.netFiatAmount
donationStats.usd.fiatAmount += donation.netFiatAmount
}
})
}
return {
props: {
project: {
...project,
content,
},
donationStats,
},
}
}

View File

@@ -1,17 +1,17 @@
import { useEffect, useState } from 'react'
import type { NextPage } from 'next'
import Head from 'next/head'
import { useEffect, useState } from 'react'
import PaymentModal from '../../components/PaymentModal'
import ProjectCard from '../../components/ProjectCard'
import { ProjectItem } from '../../utils/types'
import { getAllPosts } from '../../utils/md'
import ProjectCard from '../../../components/ProjectCard'
import { ProjectItem } from '../../../utils/types'
import { getProjects } from '../../../utils/md'
import { useFundSlug } from '../../../utils/use-fund-slug'
import { funds, fundSlugs } from '../../../utils/funds'
const AllProjects: NextPage<{ projects: ProjectItem[] }> = ({ projects }) => {
const [modalOpen, setModalOpen] = useState(false)
const [selectedProject, setSelectedProject] = useState<ProjectItem>()
const [sortedProjects, setSortedProjects] = useState<ProjectItem[]>()
const fundSlug = useFundSlug()
useEffect(() => {
setSortedProjects(projects.sort(() => 0.5 - Math.random()))
@@ -25,43 +25,47 @@ const AllProjects: NextPage<{ projects: ProjectItem[] }> = ({ projects }) => {
setSelectedProject(project)
setModalOpen(true)
}
// const projects = ["one", "two", "three", "one", "two", "three", "one", "two", "three"];
if (!fundSlug) return <></>
return (
<>
<Head>
<title>MAGIC Monero Fund | Projects</title>
</Head>
<section className="p-4 md:p-8 flex flex-col items-center">
<Head>{fundSlug && <title>{funds[fundSlug].title} | Projects</title>}</Head>
<section className="flex flex-col items-center">
<div className="flex justify-between items-center pb-8 w-full">
<h1>Projects</h1>
<h1 className="py-4 text-3xl font-extrabold leading-9 tracking-tight text-gray-900 sm:text-4xl sm:leading-10 md:text-6xl md:leading-14">
Projects
</h1>
</div>
<ul className="grid md:grid-cols-3 gap-4 max-w-5xl">
{sortedProjects &&
sortedProjects.map((p, i) => (
<li key={i} className="">
<ProjectCard project={p} openPaymentModal={openPaymentModal} />
<ProjectCard project={p} />
</li>
))}
</ul>
</section>
<PaymentModal
isOpen={modalOpen}
onRequestClose={closeModal}
project={selectedProject}
/>
</>
)
}
export default AllProjects
export async function getStaticProps({ params }: { params: any }) {
const projects = getAllPosts()
export function getStaticPaths() {
return {
paths: fundSlugs.map((fundSlug) => `/${fundSlug}/projects`),
fallback: false,
}
}
export async function getStaticProps({ params, ...asd }: { params: any }) {
const projects = await getProjects(params.fund)
return {
props: {
projects,
},
revalidate: 120,
}
}

View File

@@ -0,0 +1,155 @@
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { useRouter } from 'next/router'
import { jwtDecode } from 'jwt-decode'
import { z } from 'zod'
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '../../../components/ui/form'
import { Input } from '../../../components/ui/input'
import { Button } from '../../../components/ui/button'
import { toast } from '../../../components/ui/use-toast'
import { trpc } from '../../../utils/trpc'
import { useFundSlug } from '../../../utils/use-fund-slug'
import Spinner from '../../../components/Spinner'
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 ResetPasswordFormInputs = z.infer<typeof schema>
function ResetPassword() {
const router = useRouter()
const fundSlug = useFundSlug()
const form = useForm<ResetPasswordFormInputs>({
resolver: zodResolver(schema),
})
const resetPasswordMutation = trpc.auth.resetPassword.useMutation()
async function onSubmit(data: ResetPasswordFormInputs) {
const { token } = router.query
if (!token) return
try {
await resetPasswordMutation.mutateAsync({
token: token as string,
password: data.password,
})
toast({ title: 'Password successfully reset. You may now log in.' })
router.push(`/${fundSlug}/?loginEmail=${encodeURIComponent(data.email)}`)
} catch (error) {
const errorMessage = (error as any).message
if (errorMessage === 'INVALID_TOKEN') {
toast({
title: 'Invalid password reset link.',
variant: 'destructive',
})
router.push(`/${fundSlug}/`)
return
}
toast({
title: 'Sorry, something went wrong.',
variant: 'destructive',
})
}
}
useEffect(() => {
const { token } = router.query
if (token) {
const decoded = jwtDecode(token as string) as { email: string }
if (decoded.email) {
form.setValue('email', decoded.email)
}
}
}, [router.query.token])
return (
<div className="w-full max-w-md m-auto">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col space-y-4">
<div className="flex flex-col space-y-1.5 text-center sm:text-left">
<span className="text-lg font-semibold leading-none tracking-tight">
Password Reset
</span>
<span className="text-sm text-muted-foreground">Reset your password</span>
</div>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input {...field} disabled />
</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 && <Spinner />} Reset Password
</Button>
</form>
</Form>
</div>
)
}
export default ResetPassword

View File

@@ -0,0 +1,35 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
import { trpc } from '../../../utils/trpc'
import { useToast } from '../../../components/ui/use-toast'
import { useFundSlug } from '../../../utils/use-fund-slug'
import { signOut } from 'next-auth/react'
function VerifyEmail() {
const router = useRouter()
const { token } = router.query
const { toast } = useToast()
const fundSlug = useFundSlug()
const verifyEmailMutation = trpc.auth.verifyEmail.useMutation()
useEffect(() => {
;(async () => {
if (!token) return
try {
const result = await verifyEmailMutation.mutateAsync({ token: token as string })
toast({ title: 'Email verified! You may now log in.' })
await signOut({ callbackUrl: `/${fundSlug}/?loginEmail=${result.email}` })
} catch (error) {
toast({ title: 'Invalid verification link.', variant: 'destructive' })
router.push(`/${fundSlug}`)
}
})()
}, [token])
return <></>
}
export default VerifyEmail

View File

@@ -1,23 +1,38 @@
import '@fontsource/source-code-pro/400.css'
import '@fontsource/source-code-pro/600.css'
import '@fontsource/source-code-pro/800.css'
import '../styles/globals.css'
import '../node_modules/bootstrap/dist/css/bootstrap.css'
import { useEffect } from "react";
import type { AppProps } from 'next/app'
import { ThemeProvider } from 'next-themes'
import { SessionProvider } from 'next-auth/react'
import Head from 'next/head'
import Layout from '../components/Layout'
import { Toaster } from '../components/ui/toaster'
import { trpc } from '../utils/trpc'
import { useFundSlug } from '../utils/use-fund-slug'
import { funds } from '../utils/funds'
import '../styles/globals.css'
function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
require("../node_modules/bootstrap/dist/js/bootstrap.bundle.min.js");
}, []);
const fundSlug = useFundSlug()
return (
<Layout>
<Component {...pageProps} />
</Layout>
<SessionProvider session={pageProps.session}>
<ThemeProvider
attribute="class"
forcedTheme={fundSlug || 'general'}
themes={['monero', 'general', 'firo', 'priacyguides']}
enableSystem={false}
>
<Head>
<meta content="width=device-width, initial-scale=1" name="viewport" />
<title>{fundSlug ? funds[fundSlug].title : 'MAGIC Grants Campaigns'}</title>
</Head>
<Layout>
<Component {...pageProps} />
</Layout>
<Toaster />
</ThemeProvider>
</SessionProvider>
)
}
export default MyApp
export default trpc.withTRPC(MyApp)

View File

@@ -1,19 +0,0 @@
import markdownToHtml from '../utils/markdownToHtml'
import { getSingleFile } from '../utils/md'
import BigDumbMarkdown from '../components/BigDumbMarkdown'
export default function About({ content }: { content: string }) {
return <BigDumbMarkdown content={content} />
}
export async function getStaticProps() {
const md = getSingleFile('docs/about_us.md')
const content = await markdownToHtml(md || '')
return {
props: {
content,
},
}
}

View File

@@ -0,0 +1,85 @@
import NextAuth, { AuthOptions } from 'next-auth'
import { jwtDecode } from 'jwt-decode'
import CredentialsProvider from 'next-auth/providers/credentials'
import axios from 'axios'
import { env } from '../../../env.mjs'
import { KeycloakJwtPayload } from '../../../server/types'
import { refreshToken } from '../../../server/utils/auth'
export const authOptions: AuthOptions = {
callbacks: {
jwt: async ({ token, user, account }) => {
// On sign in
if (user && account) {
const keycloakToken = (user as any).keycloakToken
return {
sub: user.id,
email: user.email,
accessToken: keycloakToken.access_token,
accessTokenExpiresAt: Date.now() + (keycloakToken.expires_in as number) * 1000,
refreshToken: keycloakToken.refresh_token,
}
}
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpiresAt) {
return token
}
// Refresh access token
return refreshToken(token)
},
session: ({ session, token }) => {
return {
user: {
sub: token.sub,
email: token.email,
},
error: token.error,
expires: session.expires,
}
},
},
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
try {
const { data: keycloakToken } = await axios.post(
`${env.KEYCLOAK_URL}/realms/${env.KEYCLOAK_REALM_NAME}/protocol/openid-connect/token`,
new URLSearchParams({
client_id: env.KEYCLOAK_CLIENT_ID,
client_secret: env.KEYCLOAK_CLIENT_SECRET,
grant_type: 'password',
username: credentials?.email || '',
password: credentials?.password || '',
}),
{ headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
)
const keycloakTokenPayload: KeycloakJwtPayload = jwtDecode(keycloakToken.access_token)
return {
id: keycloakTokenPayload.sub,
email: keycloakTokenPayload.email,
keycloakToken,
}
} catch (error) {
const errorMessage = (error as any).response.data.error
if (errorMessage === 'invalid_grant') {
throw new Error('INVALID_CREDENTIALS')
}
}
return null
},
}),
],
}
export default NextAuth(authOptions)

View File

@@ -1,56 +0,0 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next'
import { CURRENCY, MIN_AMOUNT } from '../../config'
import { fetchPostJSONAuthed } from '../../utils/api-helpers'
import { PayReq } from '../../utils/types'
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
const { amount, project_name, project_slug, email, name }: PayReq =
req.body
const REDIRECT = 'http://monerofund.org/thankyou'
try {
// Validate the amount that was passed from the client.
if (!(amount >= MIN_AMOUNT)) {
throw new Error('Invalid amount.')
}
const metadata = {
orderId: project_slug,
project_name,
buyerName: name || 'anonymous',
buyerEmail: email || null,
}
let data = await fetchPostJSONAuthed(
`${process.env.BTCPAY_URL!}stores/${process.env.BTCPAY_STORE_ID
}/invoices`,
`token ${process.env.BTCPAY_API_KEY}`,
{
amount: amount,
currency: "USD",
metadata: {
orderId: project_slug,
project_name,
buyerName: name || 'anonymous',
buyerEmail: email || null,
posData: metadata,
},
checkout: { redirectURL: REDIRECT },
}
)
res.status(200).json(data)
} catch (err) {
console.log(err)
res.status(500).json({ statusCode: 500, message: (err as Error).message })
}
} else {
res.setHeader('Allow', 'POST')
res.status(405).end('Method Not Allowed')
}
}

157
pages/api/btcpay/webhook.ts Normal file
View File

@@ -0,0 +1,157 @@
import { NextApiRequest, NextApiResponse } from 'next'
import getRawBody from 'raw-body'
import crypto from 'crypto'
import dayjs from 'dayjs'
import {
BtcPayGetRatesRes,
BtcPayGetPaymentMethodsRes,
DonationMetadata,
} from '../../../server/types'
import { btcpayApi as _btcpayApi, btcpayApi, prisma } from '../../../server/services'
import { env } from '../../../env.mjs'
import { sendDonationConfirmationEmail } from '../../../server/utils/mailing'
export const config = {
api: {
bodyParser: false,
},
}
type BtcpayBody = Record<string, any> & {
deliveryId: string
webhookId: string
originalDeliveryId: string
isRedelivery: boolean
type: string
timestamp: number
storeId: string
invoiceId: string
metadata?: DonationMetadata
paymentMethod: string
}
async function handleBtcpayWebhook(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
res.setHeader('Allow', ['POST'])
res.status(405).end(`Method ${req.method} Not Allowed`)
return
}
if (typeof req.headers['btcpay-sig'] !== 'string') {
res.status(400).json({ success: false })
return
}
const rawBody = await getRawBody(req)
const body: BtcpayBody = JSON.parse(Buffer.from(rawBody).toString('utf8'))
const expectedSigHash = crypto
.createHmac('sha256', env.BTCPAY_WEBHOOK_SECRET)
.update(rawBody)
.digest('hex')
const incomingSigHash = (req.headers['btcpay-sig'] as string).split('=')[1]
if (expectedSigHash !== incomingSigHash) {
console.error('Invalid signature')
res.status(400).json({ success: false })
return
}
if (!body.metadata) {
return res.status(200).json({ success: true })
}
if (body.type === 'InvoicePaymentSettled') {
// Handle payments to funding required API invoices ONLY
if (body.metadata.staticGeneratedForApi === 'false') {
return res.status(200).json({ success: true })
}
// Handle payment methods like "BTC-LightningNetwork" if added in the future
const cryptoCode = body.paymentMethod.includes('-')
? body.paymentMethod.split('-')[0]
: body.paymentMethod
const { data: rates } = await btcpayApi.get<BtcPayGetRatesRes>(
`/rates?currencyPair=${cryptoCode}_USD`
)
const cryptoRate = Number(rates[0].rate)
const cryptoAmount = Number(body.payment.value)
await prisma.donation.create({
data: {
userId: null,
btcPayInvoiceId: body.invoiceId,
projectName: body.metadata.projectName,
projectSlug: body.metadata.projectSlug,
fundSlug: body.metadata.fundSlug,
cryptoCode,
netCryptoAmount: cryptoAmount,
grossCryptoAmount: cryptoAmount,
netFiatAmount: Number((cryptoAmount * cryptoRate).toFixed(2)),
grossFiatAmount: Number((cryptoAmount * cryptoRate).toFixed(2)),
},
})
}
if (body.type === 'InvoiceSettled') {
// If this is a funding required API invoice, let InvoiceReceivedPayment handle it instead
if (body.metadata.staticGeneratedForApi === 'true') {
return res.status(200).json({ success: true })
}
const { data: paymentMethods } = await btcpayApi.get<BtcPayGetPaymentMethodsRes>(
`/invoices/${body.invoiceId}/payment-methods`
)
await Promise.all(
paymentMethods.map(async (paymentMethod) => {
if (!body.metadata) return
const cryptoAmount = Number(paymentMethod.paymentMethodPaid)
if (!cryptoAmount) return
const fiatAmount = Number(paymentMethod.paymentMethodPaid) * Number(paymentMethod.rate)
await prisma.donation.create({
data: {
userId: body.metadata.userId,
btcPayInvoiceId: body.invoiceId,
projectName: body.metadata.projectName,
projectSlug: body.metadata.projectSlug,
fundSlug: body.metadata.fundSlug,
cryptoCode: paymentMethod.cryptoCode,
netCryptoAmount: cryptoAmount,
grossCryptoAmount: cryptoAmount,
netFiatAmount: Number(fiatAmount.toFixed(2)),
grossFiatAmount: Number(fiatAmount.toFixed(2)),
membershipExpiresAt:
body.metadata.isMembership === 'true' ? dayjs().add(1, 'year').toDate() : null,
},
})
if (body.metadata.donorEmail && body.metadata.donorName) {
sendDonationConfirmationEmail({
to: body.metadata.donorEmail,
donorName: body.metadata.donorName,
fundSlug: body.metadata.fundSlug,
projectName: body.metadata.projectName,
isMembership: body.metadata.isMembership === 'true',
isSubscription: false,
pointsReceived: 0,
btcpayAsset: paymentMethod.cryptoCode as 'BTC' | 'XMR',
btcpayCryptoAmount: cryptoAmount,
})
}
})
)
}
res.status(200).json({ success: true })
}
export default handleBtcpayWebhook

View File

@@ -0,0 +1,266 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { FundSlug } from '@prisma/client'
import { z } from 'zod'
import dayjs from 'dayjs'
import { getProjects } from '../../utils/md'
import { env } from '../../env.mjs'
import { btcpayApi, prisma } from '../../server/services'
import { CURRENCY } from '../../config'
import {
BtcPayCreateInvoiceRes,
BtcPayGetPaymentMethodsRes,
BtcPayGetRatesRes,
DonationMetadata,
} from '../../server/types'
import { fundSlugs } from '../../utils/funds'
const ASSETS = ['BTC', 'XMR', 'USD'] as const
type Asset = (typeof ASSETS)[number]
type ResponseBody = {
title: string
fund: FundSlug
date: string
author: string
url: string
is_funded: boolean
raised_amount_percent: number
contributions: number
target_amount_btc: number
target_amount_xmr: number
target_amount_usd: number
remaining_amount_btc: number
remaining_amount_xmr: number
remaining_amount_usd: number
address_btc: string | null
address_xmr: string | null
}[]
type ResponseBodySpecificAsset = {
title: string
fund: FundSlug
date: string
author: string
url: string
is_funded: boolean
raised_amount_percent: number
contributions: number
asset: Asset
target_amount: number
remaining_amount: number
address: string | null
}[]
// The cache key should be: fund-asset-project_status
const cachedResponses: Record<
string,
{ data: ResponseBody | ResponseBodySpecificAsset; expiresAt: Date } | undefined
> = {}
const querySchema = z.object({
fund: z.enum(fundSlugs).optional(),
asset: z.enum(ASSETS).optional(),
project_status: z.enum(['FUNDED', 'NOT_FUNDED', 'ANY']).default('NOT_FUNDED'),
})
async function handle(
req: NextApiRequest,
res: NextApiResponse<ResponseBody | ResponseBodySpecificAsset>
) {
if (req.method !== 'GET') {
res.setHeader('Allow', ['GET'])
return res.status(405).end(`Method ${req.method} Not Allowed`)
}
const query = await querySchema.parseAsync(req.query)
// Get response from cache
const cacheKey = `${query.fund}-${query.asset}-${query.project_status}`
const cachedResponse = cachedResponses[cacheKey]
if (cachedResponse && cachedResponse.expiresAt > new Date()) {
return res.send(cachedResponse.data)
}
const projects = (await getProjects(query.fund)).filter((project) =>
query.project_status === 'FUNDED'
? project.isFunded
: query.project_status === 'ANY'
? true
: !project.isFunded
)
const rates: Record<string, number | undefined> = {}
// Get exchange rates if target asset is not USD (or if there is no target asset)
if (query.asset !== 'USD') {
const assetsWithoutUsd = ASSETS.filter((asset) => asset !== 'USD')
const params = assetsWithoutUsd.map((asset) => `currencyPair=${asset}_USD`).join('&')
const { data: _rates } = await btcpayApi.get<BtcPayGetRatesRes>(`/rates?${params}`)
_rates.forEach((rate) => {
const asset = rate.currencyPair.split('_')[0] as string
rates[asset] = Number(rate.rate)
})
}
let responseBody: ResponseBody | ResponseBodySpecificAsset = await Promise.all(
projects.map(async (project): Promise<ResponseBody[0]> => {
let bitcoinAddress: string | null = null
let moneroAddress: string | null = null
if (!project.isFunded) {
const existingAddresses = await prisma.projectAddresses.findUnique({
where: { projectSlug_fundSlug: { projectSlug: project.slug, fundSlug: project.fund } },
})
// Create invoice if there's no existing address
if (!existingAddresses) {
const metadata: DonationMetadata = {
userId: null,
donorName: null,
donorEmail: null,
projectSlug: project.slug,
projectName: project.title,
fundSlug: project.fund as FundSlug,
isMembership: 'false',
isSubscription: 'false',
isTaxDeductible: 'false',
staticGeneratedForApi: 'true',
}
const { data: invoice } = await btcpayApi.post<BtcPayCreateInvoiceRes>('/invoices', {
checkout: { monitoringMinutes: 9999999 },
currency: CURRENCY,
metadata,
})
const { data: paymentMethods } = await btcpayApi.get<BtcPayGetPaymentMethodsRes>(
`/invoices/${invoice.id}/payment-methods`
)
paymentMethods.forEach((paymentMethod) => {
if (paymentMethod.paymentMethod === 'BTC') {
bitcoinAddress = paymentMethod.destination
}
if (paymentMethod.paymentMethod === 'XMR') {
moneroAddress = paymentMethod.destination
}
})
if (!bitcoinAddress && process.env.NODE_ENV !== 'development')
throw new Error(
'[/api/funding-required] Could not get bitcoin address from payment methods.'
)
if (!moneroAddress)
throw new Error(
'[/api/funding-required] Could not get monero address from payment methods.'
)
await prisma.projectAddresses.create({
data: {
projectSlug: project.slug,
fundSlug: project.fund,
btcPayInvoiceId: invoice.id,
bitcoinAddress: bitcoinAddress || '',
moneroAddress: moneroAddress,
},
})
}
if (existingAddresses) {
bitcoinAddress = existingAddresses.bitcoinAddress
moneroAddress = existingAddresses.moneroAddress
}
}
const targetAmountBtc = project.goal / (rates.BTC || 0)
const targetAmountXmr = project.goal / (rates.XMR || 0)
const targetAmountUsd = project.goal
const allDonationsSumUsd =
project.totalDonationsBTCInFiat +
project.totalDonationsXMRInFiat +
project.totalDonationsFiat
const remainingAmountBtc = (project.goal - allDonationsSumUsd) / (rates.BTC || 0)
const remainingAmountXmr = (project.goal - allDonationsSumUsd) / (rates.XMR || 0)
const remainingAmountUsd = project.goal - allDonationsSumUsd
return {
title: project.title,
fund: project.fund,
date: project.date,
author: project.nym,
url: `${env.APP_URL}/${project.fund}/${project.slug}`,
is_funded: !!project.isFunded,
target_amount_btc: Number(targetAmountBtc.toFixed(8)),
target_amount_xmr: Number(targetAmountXmr.toFixed(12)),
target_amount_usd: Number(targetAmountUsd.toFixed(2)),
remaining_amount_btc: Number((remainingAmountBtc > 0 ? remainingAmountBtc : 0).toFixed(8)),
remaining_amount_xmr: Number((remainingAmountXmr > 0 ? remainingAmountXmr : 0).toFixed(12)),
remaining_amount_usd: Number((remainingAmountUsd > 0 ? remainingAmountUsd : 0).toFixed(2)),
address_btc: bitcoinAddress,
address_xmr: moneroAddress,
raised_amount_percent: Math.floor(
((project.totalDonationsBTCInFiat +
project.totalDonationsXMRInFiat +
project.totalDonationsFiat) /
project.goal) *
100
),
contributions: project.numDonationsBTC + project.numDonationsXMR + project.numDonationsFiat,
}
})
)
if (query.asset) {
responseBody = responseBody.map<ResponseBodySpecificAsset[0]>((project) => {
const targetAmounts: Record<Asset, number> = {
BTC: project.target_amount_btc,
XMR: project.target_amount_xmr,
USD: project.target_amount_usd,
}
const remainingAmounts: Record<Asset, number> = {
BTC: project.remaining_amount_btc,
XMR: project.remaining_amount_xmr,
USD: project.remaining_amount_usd,
}
const addresses: Record<Asset, string | null> = {
BTC: project.address_btc,
XMR: project.address_xmr,
USD: null,
}
return {
title: project.title,
fund: project.fund,
date: project.date,
author: project.author,
url: project.url,
is_funded: project.is_funded,
target_amount: targetAmounts[query.asset!],
remaining_amount: remainingAmounts[query.asset!],
address: addresses[query.asset!],
raised_amount_percent: project.raised_amount_percent,
contributions: project.contributions,
asset: query.asset!,
}
})
}
// Store response in cache
cachedResponses[cacheKey] = {
data: responseBody,
expiresAt: dayjs().add(10, 'minutes').toDate(),
}
return res.send(responseBody)
}
export default handle

View File

@@ -1,43 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next/types'
const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY
const TO_ADDRESS = process.env.SENDGRID_RECEPIENT
const FROM_ADDRESS = process.env.SENDGRID_VERIFIED_SENDER
const sgMail = require('@sendgrid/mail')
sgMail.setApiKey(SENDGRID_API_KEY)
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method === 'POST') {
if (!SENDGRID_API_KEY || !TO_ADDRESS || !FROM_ADDRESS) {
throw new Error('Env misconfigured')
}
let body = ''
for (const [key, value] of Object.entries(req.body)) {
body += `<h3>${key}</h3><p>${value}</p>`
}
try {
console.log(process.env.SENDGRID_API_KEY)
const msg = {
to: TO_ADDRESS, // Change to your recipient
from: FROM_ADDRESS, // Change to your verified sender
subject: `MAGIC Monero Fund Application for ${req.body.project_name}`,
html: `${body}`,
}
sgMail.send(msg)
res.status(200).json({ message: 'success' })
} catch (err) {
res.status(500).json({ statusCode: 500, message: (err as Error).message })
}
} else {
res.setHeader('Allow', 'POST')
res.status(405).end('Method Not Allowed')
}
}

View File

@@ -0,0 +1,10 @@
import { getStripeWebhookHandler } from '../../../server/utils/webhooks'
import { env } from '../../../env.mjs'
export const config = {
api: {
bodyParser: false,
},
}
export default getStripeWebhookHandler('firo', env.STRIPE_FIRO_WEBHOOK_SECRET)

View File

@@ -0,0 +1,10 @@
import { getStripeWebhookHandler } from '../../../server/utils/webhooks'
import { env } from '../../../env.mjs'
export const config = {
api: {
bodyParser: false,
},
}
export default getStripeWebhookHandler('general', env.STRIPE_GENERAL_WEBHOOK_SECRET)

View File

@@ -0,0 +1,10 @@
import { getStripeWebhookHandler } from '../../../server/utils/webhooks'
import { env } from '../../../env.mjs'
export const config = {
api: {
bodyParser: false,
},
}
export default getStripeWebhookHandler('monero', env.STRIPE_MONERO_WEBHOOK_SECRET)

View File

@@ -0,0 +1,10 @@
import { getStripeWebhookHandler } from '../../../server/utils/webhooks'
import { env } from '../../../env.mjs'
export const config = {
api: {
bodyParser: false,
},
}
export default getStripeWebhookHandler('privacyguides', env.STRIPE_PRIVACY_GUIDES_WEBHOOK_SECRET)

View File

@@ -1,72 +0,0 @@
import { NextApiRequest, NextApiResponse } from 'next'
import { CURRENCY, MIN_AMOUNT } from '../../config'
// import { formatAmountForStripe } from '../../utils/stripe-helpers'
import Stripe from 'stripe'
import { PayReq } from '../../utils/types'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
// https://github.com/stripe/stripe-node#configuration
apiVersion: "2024-04-10",
})
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { amount, project_name, project_slug, email, name }: PayReq =
req.body
if (req.method === 'POST') {
try {
// Validate the amount that was passed from the client.
if (!(amount >= MIN_AMOUNT)) {
throw new Error('Invalid amount.')
}
// Create Checkout Sessions from body params.
const params: Stripe.Checkout.SessionCreateParams = {
mode: 'payment',
submit_type: 'donate',
currency: CURRENCY,
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: `MAGIC Grants donation: ${project_name}`,
},
unit_amount: amount*100,
},
quantity: 1,
},
],
metadata: {
donor_email: email || null,
donor_name: name || null,
project_slug: project_slug || null,
},
success_url: `${req.headers.origin}/thankyou`,
cancel_url: `${req.headers.origin}/`,
// We need metadata in here for some reason
payment_intent_data: {
metadata: {
donor_email: email || null,
donor_name: name || null,
project_slug: project_slug || null,
},
},
}
const checkoutSession: Stripe.Checkout.Session =
await stripe.checkout.sessions.create(params)
res.status(200).json(checkoutSession)
} catch (err) {
res.status(500).json({ statusCode: 500, message: (err as Error).message })
}
} else {
res.setHeader('Allow', 'POST')
res.status(405).end('Method Not Allowed')
}
}

10
pages/api/trpc/[trpc].ts Normal file
View File

@@ -0,0 +1,10 @@
import * as trpcNext from '@trpc/server/adapters/next'
import { appRouter } from '../../../server/routers/_app'
import { createContext } from '../../../server/trpc'
// export API handler
// @link https://trpc.io/docs/v11/server/adapters
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext,
})

View File

@@ -1,211 +0,0 @@
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useForm } from 'react-hook-form'
import { fetchPostJSON } from '../utils/api-helpers'
import Link from 'next/link'
export default function Apply() {
async function handleClick() { }
const [loading, setLoading] = useState(false)
const router = useRouter()
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm()
const onSubmit = async (data: any) => {
setLoading(true)
console.log(data)
const res = await fetchPostJSON('/api/sendgrid', data)
if (res.message === 'success') {
router.push('/submitted')
}
console.log(res)
setLoading(false)
}
return (
<div className="flex-1 flex flex-col items-center justify-center gap-4 py-8">
<form
onSubmit={handleSubmit(onSubmit)}
className="apply flex flex-col gap-4 p-4 max-w-2xl"
>
{/* {errors.exampleRequired && <span>This field is required</span>} */}
<div className="prose">
<h1>Application for Monero Fund Project Listing or General Fund Grant</h1>
<p>
Thanks for your interest in the Monero Fund!
</p>
<p>
We&#39;re incredibly grateful to contributors like you working to
support Monero, Bitcoin and other free and open source projects.
</p>
<p>
The MAGIC Monero Fund is offering a grant program and fundraising platform to support Monero research and development,
especially relating to privacy, security, user experience, and efficiency.
Proposals can be related to the Monero protocol directly, or they can be related to other
areas of the Monero ecosystem. For research projects, please refer to special instructions
<Link href="/apply_research"> here</Link>.
</p>
<h2>Proposal Evaluation Criteria</h2>
<div>
Submitted proposals will be evaluated by the committee based on the following criteria:
<ul>
<li><b>Impact:</b> The proposal should have a clear impact on the Monero Project.</li>
<li><b>Originality:</b> The proposal should be original and not a rehash of existing work.</li>
<li><b>Feasibility:</b> The proposal should be feasible to complete within the proposed time frame.</li>
<li><b>Quality:</b> The proposal should be well-written and well-organized.</li>
<li><b>Relevance:</b> The proposal should be relevant to the Monero Project.</li>
</ul>
</div>
<h2 id="Eligibility">Eligibility</h2>
<p>
All qualified researchers are eligible to apply, regardless of educational attainment or occupation.
However, as a nonprofit organization registered under U.S. tax laws, MAGIC Grants is required to comply
with certain laws when disbursing funds to grant recipients.
Grant recipients must complete a Due Diligence checklist,
which are the last two pages of <a href="https://magicgrants.org/funds/MAGIC%20Fund%20Grant%20Disbursement%20Process%20and%20Requirements.pdf">this document</a>.
This includes the collection of your ID and tax information. We will conduct sanctions checks.
</p>
<h2>How to Submit a Proposal</h2>
<p>
To submit a proposal, please complete the form below or create an issue on <a href="https://github.com/MAGICGrants/Monero-Fund/issues/new?assignees=&labels=&template=grant-application.md&title=[Grant+Title]">Github</a>.
Applicants are free to use their legal name or a pseudonym at this step,
although note the <a href="#Eligibility"><b>Eligibility</b></a> section.
You can choose to apply for a direct grant from the MAGIC Monero Fund&#39;s General Fund and/or
request that your project be listed on MoneroFund.org to raise funds from Monero
community members.
</p>
<p>
Please note this form is not considered confidential and is effectively equivalent to a public GitHub issue.
In order to reach out privately, please send an email to MoneroFund@magicgrants.org.
</p>
</div>
<label className="checkbox">
<input type="checkbox" {...register('general_fund')} />
Apply to receive a grant from the Magic Monero Fund.
</label>
<label className="checkbox">
<input type="checkbox" {...register('explore_page')} />
Apply for project to be listed on the Monero Fund Donation Page.
</label>
<label>
Project Name *
<small>
The name of the project to be listed for fundraising
</small>
<input
type="text"
{...register('project_name', { required: true })}
/>
</label>
<label>
Your Name or Pseudonym *
<input type="text" {...register('your_name', { required: true })} />
</label>
<label>
Email *
<input type="text" {...register('email', { required: true })} />
</label>
<label>
Project Github (if applicable)
<input type="text" {...register('github')} />
</label>
<label>
Personal Github (if applicable)
<input type="text" {...register('personal_github')} />
</label>
<label>
Other Contact Details (if applicable)
<small>
Please list any other relevant contact details you are comfortable
sharing in case we need to reach out with questions.
</small>
<small>
These could include github username, twitter username, LinkedIn,
reddit handle, other social media handles, emails, phone
numbers, usernames, etc.
</small>
<textarea {...register('other_contact')} />
</label>
<label>
Short Project Description *
<small>
This will be listed on the explore projects page of the Monero Fund
website. 2-3 sentences.
</small>
<textarea {...register('short_description', { required: true })} />
</label>
<label>
Long Project Description
<small>
This will be listed on your personal project page of the Monero Fund
website. It can be longer and go into detail about your project.
</small>
<textarea {...register('long_description')} />
</label>
{/* <label>
Project Images: (attachment)
<input type="text" {...register('your_name', { required: true })} />
</label> */}
<label className="checkbox">
<input type="checkbox" {...register('free_open_source')} />
Is the project free and open source?
</label>
<label className="checkbox">
<input type="checkbox" {...register('are_you_lead')} />
Are you the Project Lead / Lead Contributor
</label>
<label>
If someone else, please list the project&#39;s Lead Contributor or
Maintainer <input type="text" {...register('other_lead')} />
</label>
<label>
Potential Impact *
<small>
Why is this project important to the Monero community?
</small>
<textarea {...register('potential_impact', { required: true })} />
</label>
<label>
Project Timelines and Potential Milestones *
<textarea {...register('timelines', { required: true })} />
</label>
<label>
If you&#39;re applying for a grant from the general fund, please
submit a proposed budget for the requested amount and how it will be used.
<input type="text" {...register('proposed_budget')} />
</label>
<label>
Applicant Bios (Optional)
<small>
List revevant accomplishments.
</small>
<textarea {...register('bios')} />
</label>
<div className="prose">
<small>
The MAGIC Monero Fund may require each recipient to sign a Grant Agreement
before any funds are disbursed. This agreement will set milestones and funds
will only be released upon completion of milestones. In order to comply with
US regulations, recipients will need to identify themselves to MAGIC, in
accordance with US law.
</small>
</div>
<button type="submit" disabled={loading}>
Apply
</button>
<p>
After submitting your application, please allow our team up to three weeks to review your application.
Email us at <a href="mailto:monerofund@magicgrants.org">monerofund@magicgrants.org</a> if you have any questions.
</p>
</form>
</div>
)
}

View File

@@ -1,19 +0,0 @@
import markdownToHtml from '../utils/markdownToHtml'
import { getSingleFile } from '../utils/md'
import BigDumbMarkdown from '../components/BigDumbMarkdown'
export default function About({ content }: { content: string }) {
return <BigDumbMarkdown content={content} />
}
export async function getStaticProps() {
const md = getSingleFile('docs/apply_research.md')
const content = await markdownToHtml(md || '')
return {
props: {
content,
},
}
}

Some files were not shown because too many files have changed in this diff Show More