Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@fortawesome/fontawesome-svg-core": "^6.6.0",
"@fortawesome/free-brands-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.6.0",
"@fortawesome/react-fontawesome": "^0.2.2",
"@mui/icons-material": "^5.16.5",
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { auth } from "@/composition"
import { authHandlers } from "@/composition"

export const { GET, POST } = auth.handlers
export const { GET, POST } = authHandlers
127 changes: 127 additions & 0 deletions src/app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Image from "next/image"
import Link from "next/link"
import { Box, Button, Stack, Typography } from "@mui/material"
import { signIn } from "@/composition"
import { env } from "@/common"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faGithub } from "@fortawesome/free-brands-svg-icons"
import SignInTexts from "@/features/auth/view/SignInTexts"

const SITE_NAME = env.getOrThrow("NEXT_PUBLIC_SHAPE_DOCS_TITLE")
const HELP_URL = env.get("NEXT_PUBLIC_SHAPE_DOCS_HELP_URL")

export default async function SignInPage() {
return (
<Box display="flex" height="100vh">
<InfoColumn/>
<SignInColumn/>
</Box>
)
}

const InfoColumn = () => {
return (
<Box
flex={1}
sx={{ display: { xs: "none", sm: "none", md: "flex" } }}
alignItems="center"
bgcolor="#000"
color="#fff"
>
<SignInTexts />
</Box>
)
}

const SignInColumn = () => {
const title = `Get started with ${SITE_NAME}`
return (
<Box
flex={1}
display="flex"
flexDirection="column"
justifyContent="space-between"
alignItems="center"
bgcolor="#fff"
>
<Box display="flex" flex={1} justifyContent="center" alignItems="center">
<Stack direction="column" sx={{
alignItems: { xs: "center", sm: "center", md: "flex-start" },
padding: 4
}}>
<Box sx={{ marginBottom: 8 }}>
<Image
src="/images/logo.png"
alt={`${SITE_NAME} logo`}
width={150}
height={171}
priority={true}
/>
</Box>
<Typography variant="h6" sx={{
display: { xs: "flex", sm: "flex", md: "none" },
marginBottom: 3,
textAlign: "center"
}}>
{title}
</Typography>
<Typography variant="h4" sx={{
display: { xs: "none", sm: "none", md: "flex" },
marginBottom: 3
}}>
{title}
</Typography>
<SignInWithGitHub />
</Stack>
</Box>
<Box sx={{ marginBottom: 2 }}>
<Footer/>
</Box>
</Box>
)
}

const SignInWithGitHub = () => {
return (
<form
action={async () => {
"use server"
try {
await signIn("github", { redirectTo: "/" })
} catch (error) {
console.error(error)
throw error
}
}}
>
<Button variant="outlined" type="submit">
<Stack direction="row" alignItems="center" spacing={1} padding={1}>
<FontAwesomeIcon icon={faGithub} size="2xl" />
<Typography variant="h6" sx={{ display: "flex" }}>
Sign in with GitHub
</Typography>
</Stack>
</Button>
</form>
)
}

const Footer = () => {
return (
<Stack direction="row">
{HELP_URL &&
<Link href={HELP_URL} target="_blank" rel="noopener">
<Typography variant="body2" sx={{
opacity: 0.5,
transition: "opacity 0.3s ease",
"&:hover": {
opacity: 1
}
}}>
Learn more about {SITE_NAME}
</Typography>
</Link>
}
</Stack>
)
}
12 changes: 5 additions & 7 deletions src/common/session/AuthjsSession.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
import { NextAuthResult } from "next-auth"
import { Session } from "next-auth"
import { UnauthorizedError } from "@/common"
import ISession from "./ISession"

export default class AuthjsSession implements ISession {
private readonly auth: NextAuthResult
private readonly auth: () => Promise<Session | null>

constructor(config: { auth: NextAuthResult }) {
constructor(config: { auth: () => Promise<Session | null> }) {
this.auth = config.auth
}

async getIsAuthenticated(): Promise<boolean> {
const { auth } = this.auth
const session = await auth()
const session = await this.auth()
return session != null
}

async getUserId(): Promise<string> {
const { auth } = this.auth
const session = await auth()
const session = await this.auth()
if (!session || !session.user || !session.user.id) {
throw new UnauthorizedError("User ID is unavailable because the user is not authenticated.")
}
Expand Down
5 changes: 4 additions & 1 deletion src/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,17 @@ const oauthTokenRepository = new FallbackOAuthTokenRepository({

const logInHandler = new LogInHandler({ oauthTokenRepository })

export const auth = NextAuth({
export const { signIn, auth, handlers: authHandlers } = NextAuth({
adapter: PostgresAdapter(pool),
secret: env.getOrThrow("NEXTAUTH_SECRET"),
theme: {
logo: "/images/logo.png",
colorScheme: "light",
brandColor: "black"
},
pages: {
signIn: "/auth/signin"
},
providers: [
GithubProvider({
clientId: env.getOrThrow("GITHUB_CLIENT_ID"),
Expand Down
92 changes: 92 additions & 0 deletions src/features/auth/view/SignInTexts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"use client"

import { Box, Typography, SxProps } from "@mui/material"
import { useEffect, useState, useMemo } from "react"

const SignInTexts = () => {
const getRandomTextColor = ({ excluding }: { excluding?: string }) => {
const colors = ["#01BBFE", "#00AE47", "#FCB23D"]
.filter(e => e !== excluding)
return colors[Math.floor(Math.random() * colors.length)]
}
const [characterIndex, setCharacterIndex] = useState(0)
const [textIndex, setTextIndex] = useState(0)
const [displayedText, setDisplayedText] = useState("")
const [textColor, setTextColor] = useState(getRandomTextColor({}))
const texts = useMemo(() => [
"is a great OpenAPI viewer",
"facilitates spec-driven development",
"puts your documentation in one place",
"adds documentation previews to pull requests"
], [])
useEffect(() => {
const interval = setInterval(() => {
setDisplayedText("")
setCharacterIndex(0)
setTextColor(getRandomTextColor({ excluding: textColor }))
setTextIndex((prevIndex) => (prevIndex + 1) % texts.length)
}, 5000)
return () => clearInterval(interval)
}, [texts.length, textColor])
useEffect(() => {
const interval = setInterval(() => {
setCharacterIndex(characterIndex + 1)
setDisplayedText(texts[textIndex].substring(0, characterIndex))
if (characterIndex === texts[textIndex].length) {
clearInterval(interval)
}
}, 50)
return () => clearInterval(interval)
}, [texts, textIndex, characterIndex])
const longestText = texts.reduce((a, b) => (a.length > b.length ? a : b))
return (
<>
<Box sx={{ position: "relative" }}>
<Text text={longestText} sx={{ visibility: "hidden" }} />
<Text
text={displayedText}
textColor={textColor}
sx={{ position: "absolute", top: 0, left: 0, right: 0 }}
>
<Box component="span" sx={{
borderRight: `2px solid #fff`,
animation: "blink 1s step-end infinite"
}}>
&nbsp;
</Box>
</Text>
<style jsx>{`
@keyframes blink {
0%, 100% { border-color: transparent; }
50% { border-color: #fff; }
}
`}</style>
</Box>
</>
)
}

export default SignInTexts

const Text = ({
text,
textColor,
children,
sx
}: {
text: string,
textColor?: string,
children?: React.ReactNode,
sx?: SxProps
}) => {
return (
<Typography variant="h4" sx={{
...sx,
paddingLeft: { md: 5, lg: 10 },
paddingRight: { md: 5, lg: 10 }
}}>
Shape Docs <span style={{ color: textColor }}>{text}</span>
{children}
</Typography>
)
}
Binary file modified wiki/login.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.