fix´: crossbell renovation - remove useless files and fix build errors
This commit is contained in:
parent
7d3415c57a
commit
20001c1873
|
|
@ -1,57 +0,0 @@
|
|||
import { PageEmailStatus } from "@prisma/client"
|
||||
import { useForm } from "react-hook-form"
|
||||
import toast from "react-hot-toast"
|
||||
import { useStore } from "~/lib/store"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { Button } from "../ui/Button"
|
||||
import { Input } from "../ui/Input"
|
||||
import { Modal } from "../ui/Modal"
|
||||
|
||||
export const EmailPostModal: React.FC<{
|
||||
pageId: string
|
||||
}> = ({ pageId }) => {
|
||||
const [open, setOpen] = useStore((store) => [
|
||||
store.emailPostModalOpened,
|
||||
store.setEmailPostModalOpened,
|
||||
])
|
||||
|
||||
const { handleSubmit, register } = useForm({
|
||||
defaultValues: {
|
||||
subject: "",
|
||||
},
|
||||
})
|
||||
const scheduleEmailForPost = trpc.useMutation("site.scheduleEmailForPost")
|
||||
|
||||
const onSubmit = handleSubmit(async (values) => {
|
||||
await scheduleEmailForPost.mutateAsync({
|
||||
pageId,
|
||||
emailSubject: values.subject,
|
||||
})
|
||||
toast.success("Scheduled!")
|
||||
setOpen(false)
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal title="Email Post" open={open} setOpen={setOpen}>
|
||||
{
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="p-5">
|
||||
<Input
|
||||
label="Email subject"
|
||||
id="subject"
|
||||
help="Defaults to post title"
|
||||
isBlock
|
||||
{...register("subject")}
|
||||
/>
|
||||
</div>
|
||||
<div className="p-5 border-t">
|
||||
<Button type="submit" isLoading={scheduleEmailForPost.isLoading}>
|
||||
<span className="i-mdi:email-send text-xl mr-1"></span>
|
||||
<span>Send Email</span>
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
import { Button } from "~/components/ui/Button"
|
||||
import { useStore } from "~/lib/store"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { Modal } from "../ui/Modal"
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
import { DOCS_DOMAIN } from "~/lib/env"
|
||||
|
||||
export const LoginModal: React.FC = () => {
|
||||
const [loginModalOpened, setLoginModalOpened] = useStore((store) => [
|
||||
store.loginModalOpened,
|
||||
store.setLoginModalOpened,
|
||||
])
|
||||
const {
|
||||
mutate: requestLoginLink,
|
||||
status: requestLoginLinkStatus,
|
||||
data,
|
||||
error,
|
||||
} = trpc.useMutation("auth.requestLoginLink")
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
email: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit((values) => {
|
||||
requestLoginLink({
|
||||
email: values.email,
|
||||
url: location.href,
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`Continue with Email`}
|
||||
open={loginModalOpened}
|
||||
setOpen={setLoginModalOpened}
|
||||
>
|
||||
<div className="p-5">
|
||||
{data && (
|
||||
<div className="mb-5">
|
||||
We just emailed you with a link to log in, please check your inbox
|
||||
and spam folder in case you can{`'`}t find it.
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="mb-5 text-red-500">{error.message}</div>}
|
||||
<form className="space-y-5" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<label
|
||||
className="block mb-1 font-medium text-zinc-600"
|
||||
htmlFor="email"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
required
|
||||
className="input is-block"
|
||||
{...form.register("email")}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
isBlock
|
||||
isLoading={requestLoginLinkStatus === "loading"}
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-400">
|
||||
By clicking Continue, you agree to our{" "}
|
||||
<UniLink
|
||||
href={`https://${DOCS_DOMAIN}/terms.html`}
|
||||
className="underline"
|
||||
>
|
||||
Terms of Service
|
||||
</UniLink>{" "}
|
||||
and{" "}
|
||||
<UniLink
|
||||
href={`https://${DOCS_DOMAIN}/privacy.html`}
|
||||
className="underline"
|
||||
>
|
||||
Privacy Policy
|
||||
</UniLink>
|
||||
.
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginModal
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
import React, { useEffect } from "react"
|
||||
import { useStore } from "~/lib/store"
|
||||
import { Button } from "../ui/Button"
|
||||
import { Modal } from "../ui/Modal"
|
||||
import toast from "react-hot-toast"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { Input } from "../ui/Input"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
export const SubscribeModal: React.FC<{
|
||||
siteId: string
|
||||
subscription?: {
|
||||
email?: boolean
|
||||
} | null
|
||||
isLoggedIn: boolean
|
||||
}> = ({ siteId, subscription, isLoggedIn }) => {
|
||||
const router = useRouter()
|
||||
const [open, setOpen] = useStore((store) => [
|
||||
store.subscribeModalOpened,
|
||||
store.setSubscribeModalOpened,
|
||||
])
|
||||
const subscribe = trpc.useMutation("site.subscribe")
|
||||
const unsubscribe = trpc.useMutation("site.unsubscribe")
|
||||
const trpcContext = trpc.useContext()
|
||||
|
||||
const subscribeForm = useForm({
|
||||
defaultValues: {
|
||||
newUserEmail: "",
|
||||
email: subscription?.email ?? true,
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubscribe = subscribeForm.handleSubmit((values) => {
|
||||
subscribe.mutate({
|
||||
siteId,
|
||||
email: values.email,
|
||||
newUser: isLoggedIn
|
||||
? undefined
|
||||
: {
|
||||
email: values.newUserEmail,
|
||||
url: location.href,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (subscribe.isSuccess && isLoggedIn) {
|
||||
subscribe.reset()
|
||||
toast.success(subscription ? "Updated!" : "Subscribed!")
|
||||
trpcContext.invalidateQueries("site.subscription")
|
||||
}
|
||||
}, [subscribe, isLoggedIn, subscription, trpcContext])
|
||||
|
||||
useEffect(() => {
|
||||
if (unsubscribe.isSuccess) {
|
||||
unsubscribe.reset()
|
||||
toast.success("Unsubscribed!")
|
||||
trpcContext.invalidateQueries("site.subscription")
|
||||
}
|
||||
}, [unsubscribe, trpcContext])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={subscription ? `Manage your subscription` : `Become a subscriber`}
|
||||
open={open}
|
||||
setOpen={(open) => {
|
||||
setOpen(open)
|
||||
if (!open && "subscription" in router.query) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.delete("subscription")
|
||||
const search = query.toString()
|
||||
router.replace(
|
||||
`${window.location.pathname}${search ? `?${search}` : ""}`,
|
||||
)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{subscribe.isSuccess && !isLoggedIn ? (
|
||||
<div className="p-5 space-y-3">
|
||||
<p>
|
||||
We have sent you an email with a link to confirm your subscription.
|
||||
</p>
|
||||
<p>Please check your inbox (and spam folder).</p>
|
||||
</div>
|
||||
) : (
|
||||
<form className="p-5" onSubmit={handleSubscribe}>
|
||||
{!isLoggedIn && (
|
||||
<div className="mb-5">
|
||||
<Input
|
||||
label="Email"
|
||||
type="email"
|
||||
id="email"
|
||||
isBlock
|
||||
required
|
||||
{...subscribeForm.register("newUserEmail", {})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="select-none flex items-center space-x-1">
|
||||
<input type="checkbox" {...subscribeForm.register("email")} />
|
||||
<span>Receive updates via Email</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-5 space-x-3 flex items-center">
|
||||
<Button type="submit" isLoading={subscribe.isLoading}>
|
||||
<span>{subscription ? "Update" : "Subscribe"}</span>
|
||||
</Button>
|
||||
{subscription && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
isLoading={unsubscribe.isLoading}
|
||||
onClick={() => unsubscribe.mutate({ siteId })}
|
||||
>
|
||||
Unsubscribe
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
@ -6,21 +6,18 @@ import { Avatar } from "~/components/ui/Avatar"
|
|||
import { Button } from "~/components/ui/Button"
|
||||
import toast from "react-hot-toast"
|
||||
import createPica from "pica"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { UploadFile, useUploadFile } from "~/hooks/useUploadFile"
|
||||
import { updateSite } from "~/models/site.model"
|
||||
|
||||
const AvatarEditorModal: React.FC<{
|
||||
isOpen: boolean
|
||||
image?: File | null
|
||||
setIsOpen: (open: boolean) => void
|
||||
site?: string
|
||||
site: string
|
||||
uploadFile: UploadFile
|
||||
}> = ({ isOpen, setIsOpen, image, site, uploadFile }) => {
|
||||
const editorRef = useRef<ReactAvatarEditor | null>(null)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const ctx = trpc.useContext()
|
||||
const updateProfile = trpc.useMutation("user.updateProfile")
|
||||
const updateSite = trpc.useMutation("site.updateSite")
|
||||
|
||||
const cropAndSave = async () => {
|
||||
if (!editorRef.current) return
|
||||
|
|
@ -41,15 +38,15 @@ const AvatarEditorModal: React.FC<{
|
|||
const { key } = await uploadFile(blob, image!.name)
|
||||
|
||||
// Save the image to profile / site
|
||||
if (site) {
|
||||
await updateSite.mutateAsync({ site, icon: key })
|
||||
const res = await updateSite({ site, icon: key })
|
||||
|
||||
if (res.code === 0) {
|
||||
setIsOpen(false)
|
||||
toast.success("Updated!")
|
||||
} else {
|
||||
await updateProfile.mutateAsync({ avatar: key })
|
||||
toast.error("Failed to update site" + ": " + res.message)
|
||||
}
|
||||
|
||||
setIsOpen(false)
|
||||
toast.success("Updated!")
|
||||
ctx.invalidateQueries()
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
toast.error(error.message)
|
||||
|
|
@ -97,7 +94,7 @@ const AvatarEditorModal: React.FC<{
|
|||
export const AvatarForm: React.FC<{
|
||||
filename: string | undefined | null
|
||||
name: string
|
||||
site?: string
|
||||
site: string
|
||||
}> = ({ filename, name, site }) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const inputEl = useRef<HTMLInputElement>(null)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useRouter } from "next/router"
|
|||
import React, { useState, useEffect } from "react"
|
||||
import { APP_NAME } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { SEOHead } from "../common/SEOHead"
|
||||
import { DashboardIcon } from "../icons/DashboardIcon"
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import clsx from "clsx"
|
|||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { DashboardMain } from "./DashboardMain"
|
||||
import { useRouter } from "next/router"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import Link from "next/link"
|
||||
import toast from "react-hot-toast"
|
||||
import { EmptyState } from "../ui/EmptyState"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { Popover } from "@headlessui/react"
|
||||
import Link from "next/link"
|
||||
import { useEffect, useMemo } from "react"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { getUserContentsUrl } from "~/lib/user-contents"
|
||||
import { Avatar } from "../ui/Avatar"
|
||||
import type { Profile } from "unidata.js"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { logout } from "~/lib/auth.client"
|
||||
import {
|
||||
APP_DESCRIPTION,
|
||||
APP_NAME,
|
||||
|
|
@ -18,7 +17,6 @@ export function MainLayout({
|
|||
children?: React.ReactNode
|
||||
title?: string
|
||||
}) {
|
||||
const setLoginModalOpened = useStore((store) => store.setLoginModalOpened)
|
||||
const discordLink = `https://${OUR_DOMAIN}/discord`
|
||||
const companyLinks = [
|
||||
{ text: "Blog", href: `https://blog.${OUR_DOMAIN}` },
|
||||
|
|
|
|||
|
|
@ -1,14 +1,12 @@
|
|||
import clsx from "clsx"
|
||||
import React, { useEffect } from "react"
|
||||
import { getUserContentsUrl } from "~/lib/user-contents"
|
||||
import { SubscribeModal } from "../common/SubscribeModal"
|
||||
import { SEOHead } from "../common/SEOHead"
|
||||
import { SiteNavigationItem, Viewer } from "~/lib/types"
|
||||
import { SiteNavigationItem, Viewer, Profile } from "~/lib/types"
|
||||
import { SiteFooter } from "./SiteFooter"
|
||||
import { SiteHeader } from "./SiteHeader"
|
||||
import { useRouter } from "next/router"
|
||||
import { useStore } from "~/lib/store"
|
||||
import { Profile } from "unidata.js"
|
||||
|
||||
export type SiteLayoutProps = {
|
||||
site: Profile
|
||||
|
|
|
|||
|
|
@ -2,23 +2,11 @@ import type { PageType } from "~/lib/db.server"
|
|||
import { Rendered } from "~/markdown"
|
||||
import { PageContent } from "../common/PageContent"
|
||||
import { PostMeta } from "./PostMeta"
|
||||
import { Profile } from "unidata.js"
|
||||
import { Profile, Note } from "~/lib/types"
|
||||
|
||||
export const SitePage: React.FC<{
|
||||
site: Profile,
|
||||
page: {
|
||||
tags?: string[]
|
||||
date_published: string
|
||||
title?: string
|
||||
related_urls?: string[]
|
||||
authors?: string[]
|
||||
body?: {
|
||||
content?: string
|
||||
}
|
||||
metadata?: {
|
||||
owner?: string
|
||||
}
|
||||
}
|
||||
page: Note
|
||||
}> = ({ site, page }) => {
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,18 @@
|
|||
import { useCallback } from "react"
|
||||
import { WEB3_STORAGE_API_TOKEN } from "~/lib/env"
|
||||
import { Web3Storage } from 'web3.storage'
|
||||
|
||||
export const useUploadFile = () => {
|
||||
const uploadFile = useCallback<UploadFile>(
|
||||
async (blob, filename) => {
|
||||
const file = new File([blob], filename);
|
||||
const file = new File([blob], filename)
|
||||
const web3Storage = new Web3Storage({
|
||||
token: WEB3_STORAGE_API_TOKEN,
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJkaWQ6ZXRocjoweDAyMDIwODZmRjU5OUU0Y0YyMzM4MkUzNjg1Y0NmZUEyOGNBODBCOTAiLCJpc3MiOiJ3ZWIzLXN0b3JhZ2UiLCJpYXQiOjE2NTIzNjM1Njk3NDUsIm5hbWUiOiJVbmlkYXRhIn0.XmsAuXvbTj4BFhZlJK4xXfbd0ltVZJCEhqdYcW_kLOo',
|
||||
} as any)
|
||||
const cid = await web3Storage.put([file], {
|
||||
name: file.name,
|
||||
maxRetries: 3,
|
||||
wrapWithDirectory: false,
|
||||
});
|
||||
})
|
||||
return {
|
||||
key: `https://gateway.ipfs.io/ipfs/${cid}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,5 +7,4 @@ export const SITE_URL = `${IS_PROD ? "https" : "http"}://${OUR_DOMAIN}`
|
|||
export const R2_URL = process.env.NEXT_PUBLIC_R2_URL
|
||||
export const APP_DESCRIPTION = process.env.APP_DESCRIPTION
|
||||
export const DISCORD_LINK = process.env.NEXT_PUBLIC_DISCORD_LINK
|
||||
export const GITHUB_LINK = process.env.NEXT_PUBLIC_GITHUB_LINK
|
||||
export const WEB3_STORAGE_API_TOKEN = process.env.WEB3_STORAGE_API_TOKEN
|
||||
export const GITHUB_LINK = process.env.NEXT_PUBLIC_GITHUB_LINK
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
import Mailgun from "mailgun.js"
|
||||
import FormData from "form-data"
|
||||
import { singleton } from "./singleton.server"
|
||||
import {
|
||||
MAILGUN_APIKEY,
|
||||
MAILGUN_DOMAIN_NEWSLETTER,
|
||||
MAILGUN_DOMAIN_TRANSANCTION,
|
||||
} from "~/lib/env.server"
|
||||
import type { MailgunMessageData } from "mailgun.js/interfaces/Messages"
|
||||
import { IS_PROD } from "./constants"
|
||||
import { APP_NAME, OUR_DOMAIN, SITE_URL } from "./env"
|
||||
import { SubscribeFormData } from "./types"
|
||||
import { getSite } from "~/models/site.model"
|
||||
import { type Site } from "~/lib/db.server"
|
||||
import { getSiteLink } from "./helpers"
|
||||
import { generateLoginToken } from "./token.server"
|
||||
|
||||
enum MAIL_STREAM {
|
||||
NEWSLETTER = "newsletter",
|
||||
TRANSANCTION = "transaction",
|
||||
}
|
||||
|
||||
const enableMailgun = Boolean(
|
||||
MAILGUN_APIKEY && MAILGUN_DOMAIN_TRANSANCTION && MAILGUN_DOMAIN_NEWSLETTER,
|
||||
)
|
||||
|
||||
const getClient = () =>
|
||||
singleton("mailgun", () => {
|
||||
const mg = new Mailgun(FormData)
|
||||
const client = mg.client({
|
||||
username: "api",
|
||||
key: MAILGUN_APIKEY,
|
||||
timeout: 60000,
|
||||
})
|
||||
return client
|
||||
})
|
||||
|
||||
const sendEmail = async (message: MailgunMessageData, stream: MAIL_STREAM) => {
|
||||
console.log(message)
|
||||
|
||||
if (!enableMailgun) {
|
||||
console.error(
|
||||
"not sending email because no mailgun apikey or domain configured",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
await client.messages
|
||||
.create(
|
||||
stream === MAIL_STREAM.NEWSLETTER
|
||||
? MAILGUN_DOMAIN_NEWSLETTER!
|
||||
: MAILGUN_DOMAIN_TRANSANCTION!,
|
||||
message,
|
||||
)
|
||||
.then(console.log)
|
||||
}
|
||||
|
||||
const sendTransanctionEmail = async (message: MailgunMessageData) =>
|
||||
sendEmail(message, MAIL_STREAM.TRANSANCTION)
|
||||
|
||||
const sendNewsletterEmail = async (message: MailgunMessageData) =>
|
||||
sendEmail(message, MAIL_STREAM.NEWSLETTER)
|
||||
|
||||
export const sendLoginEmail = async (payload: {
|
||||
email: string
|
||||
url: string
|
||||
toSubscribeSiteId?: string
|
||||
}) => {
|
||||
const { protocol, host, pathname } = new URL(payload.url)
|
||||
const token = await generateLoginToken(
|
||||
payload.toSubscribeSiteId
|
||||
? {
|
||||
type: "subscribe",
|
||||
email: payload.email,
|
||||
siteId: payload.toSubscribeSiteId,
|
||||
}
|
||||
: {
|
||||
type: "login",
|
||||
email: payload.email,
|
||||
},
|
||||
)
|
||||
const query = new URLSearchParams([
|
||||
["token", token],
|
||||
["next", `${protocol}//${host}${pathname}`],
|
||||
])
|
||||
|
||||
const loginLink = `${
|
||||
IS_PROD ? "https" : "http"
|
||||
}://${OUR_DOMAIN}/api/login?${query.toString()}`
|
||||
|
||||
let subject = `Sign in to ${APP_NAME}`
|
||||
|
||||
let html = `
|
||||
<p>Hello,</p>
|
||||
|
||||
<p>We received a request to sign in to ${APP_NAME} using this email address, please click the link below to sign in:</p>
|
||||
|
||||
<a href="${loginLink}">Sign in to ${APP_NAME}</a>
|
||||
|
||||
<p>This link will expire in 10 minutes. If you did not request this link, you can safely ignore this email.</p>
|
||||
|
||||
<p>Thanks,</p>
|
||||
|
||||
<p>Your Proselog Team</p>
|
||||
`
|
||||
|
||||
if (payload.toSubscribeSiteId) {
|
||||
const site = await getSite(payload.toSubscribeSiteId)
|
||||
subject = `Confirm your subscription to ${site.name}`
|
||||
html = `
|
||||
<p>Please confirm your subscription to "${site.name}" by clicking the link below:</p>
|
||||
|
||||
<a href="${loginLink}">confirm</a>
|
||||
|
||||
<p>This link will expire in 7 days. If you did not request this link, you can safely ignore this email.</p>`
|
||||
}
|
||||
|
||||
const message: MailgunMessageData = {
|
||||
from: `${APP_NAME} <hi@proselog.com>`,
|
||||
to: payload.email,
|
||||
subject,
|
||||
html,
|
||||
}
|
||||
|
||||
await sendTransanctionEmail(message)
|
||||
}
|
||||
|
||||
export const sendEmailForNewPost = async (payload: {
|
||||
post: {
|
||||
emailSubject?: string | null
|
||||
slug: string
|
||||
title: string
|
||||
rendered: { contentHTML: string }
|
||||
}
|
||||
site: Site
|
||||
subscribers: { id: string; email: string }[]
|
||||
}) => {
|
||||
if (payload.subscribers.length === 0) return
|
||||
try {
|
||||
const from = `${payload.site.name} <updates@${payload.site.subdomain}.proselog.com>`
|
||||
const subject =
|
||||
payload.post.emailSubject ||
|
||||
`${payload.post.title} - ${payload.site.name}`
|
||||
|
||||
const siteLink = getSiteLink({ subdomain: payload.site.subdomain })
|
||||
const html = `
|
||||
<a href="${siteLink}/${
|
||||
payload.post.slug
|
||||
}" style="display:block;background:#eee;padding:5px 8px;border-radius:4px;text-decoration:none;">
|
||||
View this post in browser
|
||||
</a>
|
||||
|
||||
<h1 style="font-size:2.4em">${payload.post.title}</h1>
|
||||
|
||||
<div>
|
||||
${payload.post.rendered.contentHTML}
|
||||
</div>
|
||||
|
||||
|
||||
<p style="margin-top:3em;">
|
||||
<a
|
||||
href="${`${SITE_URL}/api/login?next=/ignore&token=%recipient.unsubscribeToken%`}"
|
||||
style="text-decoration:none;">
|
||||
Unsubscribe
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a
|
||||
href="${SITE_URL}"
|
||||
style="text-decoration:none;">
|
||||
Published on Proselog
|
||||
</a>
|
||||
</p>
|
||||
`
|
||||
|
||||
const recipientVariables: {
|
||||
[email in string]: { unsubscribeToken: string }
|
||||
} = {}
|
||||
|
||||
await Promise.allSettled(
|
||||
payload.subscribers.map(async (sub) => {
|
||||
const unsubscribeToken = await generateLoginToken({
|
||||
type: "unsubscribe",
|
||||
userId: sub.id,
|
||||
siteId: payload.site.id,
|
||||
})
|
||||
recipientVariables[sub.email] = {
|
||||
unsubscribeToken,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
const message: MailgunMessageData = {
|
||||
from,
|
||||
subject,
|
||||
html,
|
||||
// TODO: segment by every 800 subscribers
|
||||
to: Object.keys(recipientVariables),
|
||||
"recipient-variables": JSON.stringify(recipientVariables),
|
||||
}
|
||||
|
||||
await sendNewsletterEmail(message)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { createReactQueryHooks } from "@trpc/react"
|
||||
import { withTRPC } from "@trpc/next"
|
||||
import type { AppRouter } from "~/router"
|
||||
|
||||
export const trpc = createReactQueryHooks<AppRouter>()
|
||||
|
||||
export const wrapTrpc = ({ ssr }: { ssr?: boolean } = {}) => {
|
||||
return withTRPC({
|
||||
config() {
|
||||
const url = "/api/trpc"
|
||||
|
||||
return {
|
||||
url,
|
||||
fetch(url, init) {
|
||||
return fetch(url, {
|
||||
...init,
|
||||
credentials: "same-origin",
|
||||
})
|
||||
},
|
||||
|
||||
queryClientConfig: {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
// refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
ssr,
|
||||
})
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ import { PageVisibilityEnum, Notes } from "~/lib/types"
|
|||
import { isUUID } from "~/lib/uuid"
|
||||
import { getSite } from "./site.model"
|
||||
import { stripHTML } from "~/lib/utils"
|
||||
import { sendEmailForNewPost } from "~/lib/mailgun.server"
|
||||
import unidata from "~/lib/unidata"
|
||||
|
||||
const checkPageSlug = async ({
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { prismaPrimary, prismaRead } from "~/lib/db.server"
|
|||
import { isUUID } from "~/lib/uuid"
|
||||
import { MembershipRole, PageType, type Site } from "~/lib/db.server"
|
||||
import { Gate } from "~/lib/gate.server"
|
||||
import { sendLoginEmail } from "~/lib/mailgun.server"
|
||||
import { SiteNavigationItem, Profile } from "~/lib/types"
|
||||
import { nanoid } from "nanoid"
|
||||
import { getMembership } from "./membership"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { Toaster } from "react-hot-toast"
|
||||
import LoginModal from "~/components/common/LoginModal"
|
||||
import "~/css/main.css"
|
||||
import "~/generated/uno.css"
|
||||
import { StoreProvider, createStore } from "~/lib/store"
|
||||
import { wrapTrpc } from "~/lib/trpc"
|
||||
import {
|
||||
getDefaultWallets,
|
||||
RainbowKitProvider,
|
||||
|
|
@ -58,7 +56,6 @@ function MyApp({ Component, pageProps }: any) {
|
|||
<RainbowKitProvider chains={chains}>
|
||||
<StoreProvider createStore={createStore}>
|
||||
<Component {...pageProps} />
|
||||
<LoginModal />
|
||||
<Toaster />
|
||||
</StoreProvider>
|
||||
</RainbowKitProvider>
|
||||
|
|
@ -78,4 +75,4 @@ function MyApp({ Component, pageProps }: any) {
|
|||
// return { ...appProps }
|
||||
// }
|
||||
|
||||
export default wrapTrpc()(MyApp)
|
||||
export default MyApp
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { SiteLayout } from "~/components/site/SiteLayout"
|
||||
import { getAuthUser } from "~/lib/auth.server"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { createSSGHelpers } from "@trpc/react/ssg"
|
||||
import { appRouter } from "~/router"
|
||||
import { getTRPCContext } from "~/lib/trpc.server"
|
||||
import { SitePage } from "~/components/site/SitePage"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { getViewer } from "~/lib/viewer"
|
||||
import { Viewer } from "~/lib/types"
|
||||
import { Viewer, Profile, Note } from "~/lib/types"
|
||||
import { getUserSites, getSite } from "~/models/site.model"
|
||||
import { getPage } from "~/models/page.model"
|
||||
import { Profile, Note } from "unidata.js"
|
||||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { notFound } from "~/lib/server-side-props"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { SiteLayout } from "~/components/site/SiteLayout"
|
||||
import { getAuthUser } from "~/lib/auth.server"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { createSSGHelpers } from "@trpc/react/ssg"
|
||||
import { appRouter } from "~/router"
|
||||
import { getTRPCContext } from "~/lib/trpc.server"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { SiteArchives } from "~/components/site/SiteArchives"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { SiteHome } from "~/components/site/SiteHome"
|
||||
import { SiteLayout } from "~/components/site/SiteLayout"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { createSSGHelpers } from "@trpc/react/ssg"
|
||||
import { appRouter } from "~/router"
|
||||
import { getTRPCContext } from "~/lib/trpc.server"
|
||||
import { Viewer, Profile, Notes } from "~/lib/types"
|
||||
import { getViewer } from "~/lib/viewer"
|
||||
|
|
|
|||
|
|
@ -1,70 +0,0 @@
|
|||
import { NextApiHandler } from "next"
|
||||
import { z } from "zod"
|
||||
import { generateCookie } from "~/lib/auth.server"
|
||||
import { IS_PROD } from "~/lib/constants"
|
||||
import { prismaPrimary } from "~/lib/db.server"
|
||||
import { OUR_DOMAIN } from "~/lib/env"
|
||||
|
||||
const pathAndSearch = (pathname: string, search: string) => {
|
||||
return `${pathname}${search ? `?${search}` : ""}`
|
||||
}
|
||||
|
||||
const handler: NextApiHandler = async (req, res) => {
|
||||
const data = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
pathname: z.string().default("/"),
|
||||
search: z.string().default(""),
|
||||
host: z.string(),
|
||||
})
|
||||
.parse({
|
||||
id: req.query.id,
|
||||
pathname: req.query.pathname,
|
||||
search: req.query.search,
|
||||
host: req.headers.host,
|
||||
})
|
||||
|
||||
const isCustomDomain = data.host && !data.host.endsWith(`.${OUR_DOMAIN}`)
|
||||
|
||||
// Set cookie again for custom domain and subdomain.localhost (because *.localhost in cookie domain doesn't work)
|
||||
if (isCustomDomain || !IS_PROD) {
|
||||
const accessToken = await prismaPrimary.accessToken.findUnique({
|
||||
where: {
|
||||
publicId: data.id,
|
||||
},
|
||||
})
|
||||
|
||||
if (
|
||||
!accessToken ||
|
||||
!accessToken.publicIdExpiresAt ||
|
||||
accessToken.publicIdExpiresAt < new Date()
|
||||
) {
|
||||
throw new Error("invalid id or id expired")
|
||||
}
|
||||
|
||||
await prismaPrimary.accessToken.update({
|
||||
where: {
|
||||
id: accessToken.id,
|
||||
},
|
||||
data: {
|
||||
publicId: null,
|
||||
publicIdExpiresAt: null,
|
||||
},
|
||||
})
|
||||
|
||||
res.setHeader(
|
||||
"set-cookie",
|
||||
generateCookie({
|
||||
type: "auth",
|
||||
domain: data.host,
|
||||
token: accessToken.token,
|
||||
}),
|
||||
)
|
||||
res.redirect(pathAndSearch(data.pathname, data.search))
|
||||
return
|
||||
}
|
||||
|
||||
res.redirect(pathAndSearch(data.pathname, data.search))
|
||||
}
|
||||
|
||||
export default handler
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { prismaPrimary } from "~/lib/db.server"
|
||||
import { nanoid } from "nanoid"
|
||||
import UAParser from "ua-parser-js"
|
||||
import dayjs from "dayjs"
|
||||
import { generateCookie } from "~/lib/auth.server"
|
||||
import { OUR_DOMAIN } from "~/lib/env"
|
||||
import { IS_PROD } from "~/lib/constants"
|
||||
import { NextApiHandler } from "next"
|
||||
import { getSite, subscribeToSite } from "~/models/site.model"
|
||||
import { createGate } from "~/lib/gate.server"
|
||||
import { decryptLoginToken } from "~/lib/token.server"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
|
||||
const handler: NextApiHandler = async (req, res) => {
|
||||
const data = z
|
||||
.object({
|
||||
token: z.string(),
|
||||
next: z.string(),
|
||||
userAgent: z.string(),
|
||||
})
|
||||
.parse({
|
||||
token: req.query.token,
|
||||
next: req.query.next,
|
||||
userAgent: req.headers["user-agent"],
|
||||
})
|
||||
|
||||
const payload = await decryptLoginToken(data.token)
|
||||
|
||||
let user = await prismaPrimary.user.findUnique({
|
||||
where:
|
||||
payload.type === "unsubscribe"
|
||||
? {
|
||||
id: payload.userId,
|
||||
}
|
||||
: {
|
||||
email: payload.email,
|
||||
},
|
||||
include: {
|
||||
memberships: true,
|
||||
},
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
if (payload.type === "unsubscribe") {
|
||||
throw new Error("User not found")
|
||||
} else {
|
||||
user = await prismaPrimary.user.create({
|
||||
data: {
|
||||
email: payload.email,
|
||||
name: payload.email.split("@")[0],
|
||||
username: nanoid(7),
|
||||
},
|
||||
include: {
|
||||
memberships: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.type === "subscribe") {
|
||||
// Subscribe the user to the site
|
||||
const gate = createGate({ user })
|
||||
await subscribeToSite(gate, {
|
||||
siteId: payload.siteId,
|
||||
email: true,
|
||||
})
|
||||
}
|
||||
|
||||
const ua = new UAParser(data.userAgent)
|
||||
|
||||
const publicId = nanoid(32)
|
||||
const accessToken = await prismaPrimary.accessToken.create({
|
||||
data: {
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
token: nanoid(32),
|
||||
name: `Login via ${ua.getOS().name} ${ua.getBrowser().name}`,
|
||||
publicId,
|
||||
publicIdExpiresAt: dayjs().add(1, "minute").toDate(),
|
||||
},
|
||||
})
|
||||
|
||||
if (!IS_PROD) {
|
||||
console.log("login with token", accessToken.token)
|
||||
}
|
||||
|
||||
if (payload.type === "unsubscribe") {
|
||||
const site = await getSite(payload.siteId)
|
||||
data.next = `${getSiteLink({
|
||||
subdomain: site.subdomain,
|
||||
})}?subscription`
|
||||
}
|
||||
|
||||
const nextUrl = new URL(data.next)
|
||||
|
||||
// Custom domain
|
||||
if (nextUrl.host !== OUR_DOMAIN && !nextUrl.host.endsWith(`.${OUR_DOMAIN}`)) {
|
||||
// Check if the host belong to a site
|
||||
// const existing = await prismaPrimary.domain.findUnique({
|
||||
// where: {
|
||||
// domain: nextUrl.hostname,
|
||||
// },
|
||||
// })
|
||||
// if (!existing) {
|
||||
// throw new Error("invalid next url")
|
||||
// }
|
||||
throw new Error("invalid next url")
|
||||
}
|
||||
|
||||
nextUrl.searchParams.set("id", publicId)
|
||||
nextUrl.searchParams.set("pathname", nextUrl.pathname)
|
||||
nextUrl.pathname = "/api/login-complete"
|
||||
console.log("redirecting to", nextUrl.href)
|
||||
|
||||
res.setHeader(
|
||||
"set-cookie",
|
||||
generateCookie({
|
||||
type: "auth",
|
||||
domain: OUR_DOMAIN,
|
||||
token: accessToken.token,
|
||||
}),
|
||||
)
|
||||
|
||||
res.redirect(nextUrl.href)
|
||||
}
|
||||
|
||||
export default handler
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { NextApiHandler } from "next"
|
||||
import { generateCookie } from "~/lib/auth.server"
|
||||
|
||||
const handler: NextApiHandler = async (req, res) => {
|
||||
res.setHeader("set-cookie", generateCookie({ type: "clear" }))
|
||||
const next = req.query.next as string | undefined
|
||||
if (next) {
|
||||
res.redirect(next)
|
||||
} else {
|
||||
res.redirect("/")
|
||||
}
|
||||
}
|
||||
|
||||
export default handler
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
import * as trpcNext from "@trpc/server/adapters/next"
|
||||
import { getTRPCContext } from "~/lib/trpc.server"
|
||||
|
||||
import { appRouter } from "~/router"
|
||||
|
||||
// export API handler
|
||||
export default trpcNext.createNextApiHandler({
|
||||
router: appRouter,
|
||||
createContext: getTRPCContext,
|
||||
})
|
||||
|
|
@ -20,7 +20,6 @@ import { getPageVisibility } from "~/lib/page-helpers"
|
|||
import { PageVisibilityEnum, Note } from "~/lib/types"
|
||||
import { FieldLabel } from "~/components/ui/FieldLabel"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { EmailPostModal } from "~/components/common/EmailPostModal"
|
||||
import { useStore } from "~/lib/store"
|
||||
import { getPage, createOrUpdatePage } from "~/models/page.model"
|
||||
|
||||
|
|
@ -286,7 +285,6 @@ export default function SubdomainEditor() {
|
|||
</div>
|
||||
</DashboardMain>
|
||||
</DashboardLayout>
|
||||
{pageId && <EmailPostModal pageId={pageId} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import toast from "react-hot-toast"
|
|||
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
|
||||
import { useRouter } from "next/router"
|
||||
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { getSite, updateSite } from "~/models/site.model"
|
||||
import { Profile } from "unidata.js"
|
||||
|
|
@ -64,7 +63,7 @@ export default function SiteSettingsGeneralPage() {
|
|||
<div>
|
||||
<label className="form-label">Icon</label>
|
||||
<AvatarForm
|
||||
site={site.username}
|
||||
site={site.username!}
|
||||
name={site.name || site.username || ""}
|
||||
filename={site.avatars?.[0] || undefined}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import toast from "react-hot-toast"
|
|||
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
|
||||
import { useRouter } from "next/router"
|
||||
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { SiteNavigationItem, Profile } from "~/lib/types"
|
||||
import { nanoid } from "nanoid"
|
||||
import { ReactSortable } from "react-sortablejs"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { SEOHead } from "~/components/common/SEOHead"
|
|||
import { Button } from "~/components/ui/Button"
|
||||
import { Input } from "~/components/ui/Input"
|
||||
import { APP_NAME, OUR_DOMAIN } from "~/lib/env"
|
||||
import { trpc } from "~/lib/trpc"
|
||||
import { useAccount } from "wagmi"
|
||||
import { createSite } from "~/models/site.model"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { sendLoginEmail } from "~/lib/mailgun.server"
|
||||
import { createRouter } from "~/lib/trpc.server"
|
||||
import { getViewer } from "~/lib/viewer"
|
||||
|
||||
export const authRouter = createRouter()
|
||||
.query("viewer", {
|
||||
output: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
name: z.string(),
|
||||
username: z.string(),
|
||||
avatar: z.string().nullish(),
|
||||
bio: z.string().nullish(),
|
||||
})
|
||||
.nullable(),
|
||||
async resolve({ ctx }) {
|
||||
return getViewer(ctx.user)
|
||||
},
|
||||
})
|
||||
.mutation("requestLoginLink", {
|
||||
input: z.object({
|
||||
email: z.string().nonempty("email is required"),
|
||||
url: z.string(),
|
||||
}),
|
||||
output: z.boolean(),
|
||||
async resolve({ input }) {
|
||||
await sendLoginEmail({
|
||||
url: input.url,
|
||||
email: input.email,
|
||||
}).catch(console.error)
|
||||
|
||||
return true
|
||||
},
|
||||
})
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import * as trpc from "@trpc/server"
|
||||
import { z, ZodError } from "zod"
|
||||
import { isNotFoundError } from "~/lib/server-side-props"
|
||||
import { TRPCContext } from "~/lib/trpc.server"
|
||||
import { getSite } from "~/models/site.model"
|
||||
import { authRouter } from "./auth"
|
||||
import { membershipRouter } from "./membership"
|
||||
import { pageRouter } from "./page"
|
||||
import { siteRouter } from "./site"
|
||||
import { userRouter } from "./user"
|
||||
|
||||
export const appRouter = trpc
|
||||
.router<TRPCContext>()
|
||||
.query("site", {
|
||||
input: z.object({
|
||||
site: z.string(),
|
||||
}),
|
||||
output: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
icon: z.string().nullable(),
|
||||
subdomain: z.string(),
|
||||
navigation: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
)
|
||||
.nullable(),
|
||||
}),
|
||||
async resolve({ input }) {
|
||||
const site = await getSite(input.site)
|
||||
return site
|
||||
},
|
||||
})
|
||||
.merge("auth.", authRouter)
|
||||
.merge("site.", siteRouter)
|
||||
.merge("user.", userRouter)
|
||||
.merge("membership.", membershipRouter)
|
||||
.merge("page.", pageRouter)
|
||||
.formatError(({ error, shape }) => {
|
||||
const isZodError = error.cause instanceof ZodError
|
||||
return {
|
||||
...shape,
|
||||
message: isZodError
|
||||
? error.cause.issues.map((i) => i.message).join(", ")
|
||||
: error.message,
|
||||
data: {
|
||||
...shape.data,
|
||||
notFound: isNotFoundError(error.cause),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
// export type definition of API
|
||||
export type AppRouter = typeof appRouter
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { createRouter } from "~/lib/trpc.server"
|
||||
import { updateMembership } from "~/models/membership"
|
||||
|
||||
export const membershipRouter = createRouter().mutation("updateMembership", {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
lastSwitchedTo: z
|
||||
.string()
|
||||
.transform((v) => new Date())
|
||||
.optional(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
const { id, ...payload } = input
|
||||
if (
|
||||
!ctx.gate.allows({ type: "can-update-membership", membership: { id } })
|
||||
) {
|
||||
throw ctx.gate.permissionError()
|
||||
}
|
||||
|
||||
await updateMembership(id, payload)
|
||||
},
|
||||
})
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { createRouter } from "~/lib/trpc.server"
|
||||
import { createOrUpdatePage, deletePage } from "~/models/page.model"
|
||||
|
||||
export const pageRouter = createRouter()
|
||||
.query("authors", {
|
||||
resolve() {},
|
||||
})
|
||||
.mutation("createOrUpdate", {
|
||||
input: z.object({
|
||||
siteId: z.string(),
|
||||
pageId: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
content: z.string().optional(),
|
||||
published: z.boolean().optional(),
|
||||
publishedAt: z.string().optional(),
|
||||
excerpt: z.string().optional(),
|
||||
isPost: z.boolean().optional(),
|
||||
slug: z.string().optional(),
|
||||
emailSubject: z.string().optional(),
|
||||
}),
|
||||
output: z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
const { page } = await createOrUpdatePage(ctx.gate, input)
|
||||
return page
|
||||
},
|
||||
})
|
||||
.mutation("delete", {
|
||||
input: z.object({
|
||||
pageId: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
await deletePage(ctx.gate, { id: input.pageId })
|
||||
},
|
||||
})
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
import { z } from "zod"
|
||||
import { createRouter } from "~/lib/trpc.server"
|
||||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import {
|
||||
getSite,
|
||||
getSubscription,
|
||||
updateSite,
|
||||
createSite,
|
||||
subscribeToSite,
|
||||
unsubscribeFromSite,
|
||||
} from "~/models/site.model"
|
||||
import {
|
||||
getPage,
|
||||
getPagesBySite,
|
||||
scheduleEmailForPost,
|
||||
} from "~/models/page.model"
|
||||
|
||||
export const siteRouter = createRouter()
|
||||
.query("subscription", {
|
||||
input: z.object({
|
||||
site: z.string(),
|
||||
}),
|
||||
output: z
|
||||
.object({
|
||||
email: z.boolean().optional(),
|
||||
})
|
||||
.nullable(),
|
||||
async resolve({ input, ctx }) {
|
||||
const user = ctx.gate.getUser()
|
||||
if (!user) return null
|
||||
const site = await getSite(input.site)
|
||||
const subscription = await getSubscription({
|
||||
siteId: site.id,
|
||||
userId: user.id,
|
||||
})
|
||||
return subscription ? subscription.config : null
|
||||
},
|
||||
})
|
||||
.query("pages", {
|
||||
input: z.object({
|
||||
site: z.string(),
|
||||
type: z.enum(["post", "page"]).default("post"),
|
||||
visibility: z
|
||||
.enum([
|
||||
PageVisibilityEnum.All,
|
||||
PageVisibilityEnum.Published,
|
||||
PageVisibilityEnum.Draft,
|
||||
PageVisibilityEnum.Scheduled,
|
||||
])
|
||||
.nullish(),
|
||||
take: z.number().optional(),
|
||||
cursor: z.string().optional(),
|
||||
includeContent: z.boolean().optional(),
|
||||
includeExcerpt: z.boolean().optional(),
|
||||
}),
|
||||
output: z.object({
|
||||
list: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
body: z.object({
|
||||
content: z.string(),
|
||||
}),
|
||||
date_published: z.date().transform((v) => v.toISOString()),
|
||||
summary: z.object({
|
||||
content: z.string(),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
total: z.number(),
|
||||
hasMore: z.boolean(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
const result = await getPagesBySite(input)
|
||||
return result
|
||||
},
|
||||
})
|
||||
.query("page", {
|
||||
input: z.object({
|
||||
site: z.string(),
|
||||
page: z.string(),
|
||||
render: z.boolean(),
|
||||
includeAuthors: z.boolean().optional(),
|
||||
}),
|
||||
output: z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
content: z.string(),
|
||||
excerpt: z.string().nullable(),
|
||||
published: z.boolean(),
|
||||
publishedAt: z.date().transform((v) => v.toISOString()),
|
||||
slug: z.string(),
|
||||
type: z.enum(["PAGE", "POST"]),
|
||||
rendered: z
|
||||
.object({
|
||||
excerpt: z.string(),
|
||||
contentHTML: z.string(),
|
||||
})
|
||||
.nullable(),
|
||||
authors: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
avatar: z.string().nullable(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
emailSubject: z.string().nullish(),
|
||||
emailStatus: z.string().nullish(),
|
||||
siteId: z.string(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
const page = await getPage(ctx.gate, input)
|
||||
return page
|
||||
},
|
||||
})
|
||||
.mutation("updateSite", {
|
||||
input: z.object({
|
||||
site: z.string(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
icon: z.string().nullish(),
|
||||
subdomain: z.string().optional(),
|
||||
navigation: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
url: z
|
||||
.string()
|
||||
.regex(
|
||||
/^(https?:\/\/|\/)/,
|
||||
"URL must start with / or http:// or https://",
|
||||
),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
output: z.object({
|
||||
site: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
subdomain: z.string(),
|
||||
}),
|
||||
subdomainUpdated: z.boolean(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
const { site, subdomainUpdated } = await updateSite(ctx.gate, input)
|
||||
return {
|
||||
site,
|
||||
subdomainUpdated,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
.mutation("create", {
|
||||
input: z.object({
|
||||
name: z.string(),
|
||||
subdomain: z.string().min(3).max(26),
|
||||
}),
|
||||
output: z.object({
|
||||
id: z.string(),
|
||||
subdomain: z.string(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
const { site } = await createSite(ctx.gate, input)
|
||||
return site
|
||||
},
|
||||
})
|
||||
.mutation("subscribe", {
|
||||
input: z.object({
|
||||
email: z.boolean().optional(),
|
||||
siteId: z.string(),
|
||||
newUser: z
|
||||
.object({
|
||||
email: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
await subscribeToSite(ctx.gate, input)
|
||||
},
|
||||
})
|
||||
.mutation("unsubscribe", {
|
||||
input: z.object({
|
||||
siteId: z.string(),
|
||||
}),
|
||||
async resolve({ input, ctx }) {
|
||||
await unsubscribeFromSite(ctx.gate, input)
|
||||
},
|
||||
})
|
||||
.mutation("scheduleEmailForPost", {
|
||||
input: z.object({
|
||||
pageId: z.string(),
|
||||
emailSubject: z.string().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
await scheduleEmailForPost(ctx.gate, {
|
||||
pageId: input.pageId,
|
||||
emailSubject: input.emailSubject,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { MembershipRole } from "@prisma/client"
|
||||
import { encrypt, getDerivedKey } from "@proselog/jwt"
|
||||
import { z } from "zod"
|
||||
import { IS_PROD } from "~/lib/constants"
|
||||
import { ENCRYPT_SECRET } from "~/lib/env.server"
|
||||
import { createRouter } from "~/lib/trpc.server"
|
||||
import { getMemberships } from "~/models/membership"
|
||||
import { userModel } from "~/models/user.model"
|
||||
|
||||
export const userRouter = createRouter()
|
||||
.query("getSignedJwt", {
|
||||
output: z.string(),
|
||||
async resolve({ ctx }) {
|
||||
const user = ctx.gate.getUser(true)
|
||||
const key = await getDerivedKey(ENCRYPT_SECRET)
|
||||
const token = await encrypt(
|
||||
{ prefix: IS_PROD ? `${user.id}/` : `dev/${user.id}/` },
|
||||
key,
|
||||
{
|
||||
expiresIn: "1h",
|
||||
}
|
||||
)
|
||||
return token
|
||||
},
|
||||
})
|
||||
.query("getSubscriptions", {
|
||||
input: z.object({
|
||||
canManage: z.boolean().optional(),
|
||||
}),
|
||||
output: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
role: z.enum([
|
||||
MembershipRole.ADMIN,
|
||||
MembershipRole.OWNER,
|
||||
MembershipRole.SUBSCRIBER,
|
||||
]),
|
||||
site: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
subdomain: z.string(),
|
||||
icon: z.string().nullable(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
async resolve({ ctx, input }) {
|
||||
const user = ctx.gate.getUser(true)
|
||||
return getMemberships({ userId: user.id, ...input })
|
||||
},
|
||||
})
|
||||
.mutation("updateProfile", {
|
||||
input: z.object({
|
||||
username: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
bio: z.string().optional(),
|
||||
avatar: z.string().optional(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
await userModel.updateProfile(ctx.gate, input)
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue