post list fixes, list skeletons, migrate date picker to shadui

pull/150/head
Max Leiter 3 years ago
parent 629f9e30f8
commit ff6014ad06

@ -8,6 +8,8 @@ You can try a demo at https://drift.lol. The demo is built on main but has no da
If you want to contribute, need support, or want to stay updated, you can join the IRC channel at #drift on irc.libera.chat or [reach me on twitter](https://twitter.com/Max_Leiter). If you don't have an IRC client yet, you can use a webclient [here](https://demo.thelounge.chat/#/connect?join=%23drift&nick=drift-user&realname=Drift%20User).
Drift is built with Next.js 13, React Server Components, [shadcn/ui](https://github.com/shadcn/ui), and [Prisma](https://prisma.io/).
<hr />
**Contents:**
@ -48,6 +50,7 @@ You can change these to your liking.
- `NODE_ENV`: defaults to development, can be `production`
#### Auth environment variables
**Note:** Only credential auth currently supports the registration password, so if you want to secure registration, you must use only credential auth.
- `GITHUB_CLIENT_ID`: the client ID for GitHub OAuth.

@ -0,0 +1,16 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true
},
"aliases": {
"components": "@components",
"utils": "@utils"
}
}

@ -14,8 +14,8 @@
},
"dependencies": {
"@next-auth/prisma-adapter": "^1.0.7",
"@next/eslint-plugin-next": "13.4.4-canary.0",
"@prisma/client": "^4.15.0",
"@next/eslint-plugin-next": "13.4.9",
"@prisma/client": "^4.16.2",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-dropdown-menu": "^2.0.4",
@ -32,12 +32,14 @@
"date-fns": "^2.30.0",
"jest": "^29.5.0",
"lodash.debounce": "^4.0.8",
"next": "13.4.5-canary.2",
"lucide-react": "^0.259.0",
"next": "13.4.9",
"next-auth": "^4.22.1",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-cookie": "^4.1.1",
"react-datepicker": "4.10.0",
"react-day-picker": "^8.8.0",
"react-dom": "18.2.0",
"react-dropzone": "14.2.3",
"react-error-boundary": "^4.0.4",
@ -52,7 +54,7 @@
"uuid": "^9.0.0"
},
"devDependencies": {
"@next/bundle-analyzer": "13.4.5-canary.2",
"@next/bundle-analyzer": "13.4.9",
"@total-typescript/ts-reset": "^0.4.2",
"@types/bcrypt": "^5.0.0",
"@types/git-http-backend": "^1.0.1",
@ -72,7 +74,7 @@
"csstype": "^3.1.2",
"dotenv": "^16.0.3",
"eslint": "8.38.0",
"eslint-config-next": "13.4.5-canary.2",
"eslint-config-next": "13.4.9",
"jest-mock-extended": "^3.0.3",
"next-unused": "0.0.6",
"postcss": "^8.4.21",
@ -82,9 +84,9 @@
"postcss-preset-env": "^8.4.1",
"prettier": "2.8.7",
"prettier-plugin-tailwindcss": "^0.3.0",
"prisma": "^4.15.0",
"prisma": "^4.16.2",
"tailwindcss": "^3.3.2",
"typescript": "5.0.4",
"typescript": "5.1.6",
"typescript-plugin-css-modules": "5.0.1"
},
"optionalDependencies": {

File diff suppressed because it is too large Load Diff

@ -1,21 +1,8 @@
.container {
padding: 2rem 2rem;
border-radius: var(--radius);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
}
.form {
display: grid;
place-items: center;
}
.formGroup {
display: flex;
flex-direction: column;
place-items: center;
gap: 10px;
max-width: 300px;
width: 100%;
}
.formContentSpace {

@ -13,6 +13,7 @@ import Note from "@components/note"
import { ErrorQueryParamsHandler } from "./query-handler"
import { AuthProviders } from "@lib/server/auth-props"
import { TypographyH1 } from "@components/typography"
import { cn } from "@lib/cn"
function Auth({
page,
@ -54,6 +55,7 @@ function Auth({
setSubmitting(false)
} else {
router.push("/new")
router.refresh()
}
}
@ -74,7 +76,7 @@ function Auth({
return (
<div className={styles.container}>
<ErrorQueryParamsHandler />
<div className={styles.form}>
<div className={"mx-auto w-[300px]"}>
<div className={styles.formContentSpace}>
<h1 className="text-3xl font-bold">Sign {signText}</h1>
</div>
@ -123,28 +125,30 @@ function Auth({
width="100%"
aria-label="Password"
/>
<Button type="submit">Sign {signText}</Button>
<Button type="submit" loading={submitting}>Sign {signText}</Button>
</>
) : null}
{authProviders?.length ? (
<>
<hr className="w-full" />
<p className="p-0 mt-2 text-center">
Or sign {signText.toLowerCase()} with one of the following
</p>
{authProviders?.map((provider) => {
return provider.enabled ? (
<Button
type="submit"
key={provider.id + "-button"}
style={{
color: "var(--fg)"
}}
onClick={(e) => {
e.preventDefault()
signIn(provider.id, {
callbackUrl: "/",
registration_password: serverPassword
})
router.refresh();
router.refresh()
}}
className="my-2 flex w-full max-w-[250px] items-center justify-center"
>
{getProviderIcon(provider.id)} Sign{" "}
{signText.toLowerCase()} with {provider.public_name}
@ -182,10 +186,10 @@ export default Auth
const getProviderIcon = (provider: string) => {
switch (provider) {
case "github":
return <GitHub />
return <GitHub className="w-5 h-5 mr-2" />
case "keycloak":
return <Key />
return <Key className="w-5 h-5 mr-2" />
default:
return <User />
return <User className="w-5 h-5 mr-2" />
}
}

@ -2,14 +2,17 @@ import { getMetadata } from "src/app/lib/metadata"
import Auth from "../components"
import { getAuthProviders, isCredentialEnabled } from "@lib/server/auth-props"
import { PageWrapper } from "@components/page-wrapper"
export default function SignInPage() {
return (
<Auth
page="signin"
credentialAuth={isCredentialEnabled()}
authProviders={getAuthProviders()}
/>
<PageWrapper>
<Auth
page="signin"
credentialAuth={isCredentialEnabled()}
authProviders={getAuthProviders()}
/>
</PageWrapper>
)
}

@ -2,6 +2,7 @@ import Auth from "../components"
import { getMetadata } from "src/app/lib/metadata"
import { getAuthProviders, isCredentialEnabled } from "@lib/server/auth-props"
import { getRequiresPasscode } from "src/app/api/auth/requires-passcode/route"
import { PageWrapper } from "@components/page-wrapper"
async function getPasscode() {
return getRequiresPasscode()
@ -10,12 +11,14 @@ async function getPasscode() {
export default async function SignUpPage() {
const requiresPasscode = await getPasscode()
return (
<Auth
page="signup"
requiresServerPassword={requiresPasscode}
credentialAuth={isCredentialEnabled()}
authProviders={getAuthProviders()}
/>
<PageWrapper>
<Auth
page="signup"
requiresServerPassword={requiresPasscode}
credentialAuth={isCredentialEnabled()}
authProviders={getAuthProviders()}
/>
</PageWrapper>
)
}

@ -1,4 +1,4 @@
import { Popover } from "@components/popover"
import { Popover, PopoverContent, PopoverTrigger } from "@components/popover"
import { codeFileExtensions } from "@lib/constants"
import type { PostWithFiles } from "src/lib/server/prisma"
import styles from "./dropdown.module.css"
@ -18,7 +18,7 @@ function FileDropdown({
if (loading) {
return (
<Popover>
<Popover.Trigger
<PopoverTrigger
className={buttonVariants({
variant: "link"
})}
@ -26,7 +26,7 @@ function FileDropdown({
<div style={{ minWidth: 125 }}>
<Spinner />
</div>
</Popover.Trigger>
</PopoverTrigger>
</Popover>
)
}
@ -71,7 +71,7 @@ function FileDropdown({
return (
<Popover>
<Popover.Trigger
<PopoverTrigger
className={buttonVariants({
variant: "secondary"
})}
@ -82,10 +82,10 @@ function FileDropdown({
<span>
Jump to {files.length} {files.length === 1 ? "file" : "files"}
</span>
</Popover.Trigger>
<Popover.Content className={styles.contentWrapper}>
</PopoverTrigger>
<PopoverContent className={styles.contentWrapper}>
{content}
</Popover.Content>
</PopoverContent>
</Popover>
)
}

@ -20,11 +20,27 @@ import dynamic from "next/dynamic"
import ButtonDropdown from "@components/button-dropdown"
import clsx from "clsx"
import { Spinner } from "@components/spinner"
const DatePicker = dynamic(() => import("react-datepicker"), {
ssr: false,
loading: () => <Input placeholder="Won't expire" width="100%" height="40px" />
})
import { cn } from "@lib/cn"
import { Calendar as CalendarIcon } from "react-feather"
const DatePicker = dynamic(
() => import("@components/date-picker").then((m) => m.DatePicker),
{
ssr: false,
loading: () => (
<Button
variant={"outline"}
className={cn(
"w-[280px] justify-start text-left font-normal",
"text-muted-foreground"
)}
>
<CalendarIcon className="w-4 h-4 mr-2" />
<span>Won't expire</span>
</Button>
)
}
)
const emptyDoc = {
title: "",
@ -273,24 +289,7 @@ function Post({
>
Add a File
</Button>
<DatePicker
onChange={onChangeExpiration}
customInput={
<Input hideLabel label="Expires at" width="100%" height="40px" />
}
placeholderText="Won't expire"
selected={expiresAt}
showTimeInput={true}
// @ts-expect-error fix time input type
customTimeInput={<CustomTimeInput />}
timeInputLabel="Time:"
dateFormat="MM/dd/yyyy h:mm aa"
clearButtonTitle={"Clear"}
// TODO: investigate why this causes margin shift if true
enableTabLoop={false}
minDate={new Date()}
className="max-w-[200px] flex-1"
/>
<DatePicker setExpiresAt={setExpiresAt} expiresAt={expiresAt} />
</span>
<ButtonDropdown>
<span

@ -1,391 +0,0 @@
.react-datepicker__year-read-view--down-arrow,
.react-datepicker__month-read-view--down-arrow,
.react-datepicker__month-year-read-view--down-arrow,
.react-datepicker__navigation-icon::before {
border-color: var(--light-gray);
border-style: solid;
border-width: 3px 3px 0 0;
content: "";
display: block;
height: 9px;
position: absolute;
top: 6px;
width: 9px;
}
.react-datepicker-popper[data-placement^="top"] .react-datepicker__triangle,
.react-datepicker-popper[data-placement^="bottom"] .react-datepicker__triangle {
margin-left: -4px;
position: absolute;
width: 0;
}
.react-datepicker-wrapper {
display: inline-block;
padding: 0;
border: 0;
}
.react-datepicker {
font-family: var(--font-sans);
font-size: 0.8rem;
background-color: var(--bg);
color: var(--fg);
border: 1px solid var(--gray);
border-radius: var(--radius);
display: inline-block;
position: relative;
}
.react-datepicker--time-only .react-datepicker__triangle {
left: 35px;
}
.react-datepicker--time-only .react-datepicker__time-container {
border-left: 0;
}
.react-datepicker--time-only .react-datepicker__time,
.react-datepicker--time-only .react-datepicker__time-box {
border-radius: var(--radius);
border-radius: var(--radius);
}
.react-datepicker__triangle {
position: absolute;
left: 50px;
}
.react-datepicker-popper {
z-index: 1;
}
.react-datepicker-popper[data-placement^="bottom"] {
padding-top: 10px;
}
.react-datepicker-popper[data-placement="bottom-end"]
.react-datepicker__triangle,
.react-datepicker-popper[data-placement="top-end"] .react-datepicker__triangle {
left: auto;
right: 50px;
}
.react-datepicker-popper[data-placement^="top"] {
padding-bottom: 10px;
}
.react-datepicker-popper[data-placement^="right"] {
padding-left: 8px;
}
.react-datepicker-popper[data-placement^="right"] .react-datepicker__triangle {
left: auto;
right: 42px;
}
.react-datepicker-popper[data-placement^="left"] {
padding-right: 8px;
}
.react-datepicker-popper[data-placement^="left"] .react-datepicker__triangle {
left: 42px;
right: auto;
}
.react-datepicker__header {
text-align: center;
background-color: var(--bg);
border-bottom: 1px solid var(--gray);
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
padding: 8px 0;
position: relative;
}
.react-datepicker__header--time {
padding-bottom: 8px;
padding-left: 5px;
padding-right: 5px;
}
.react-datepicker__year-dropdown-container--select,
.react-datepicker__month-dropdown-container--select,
.react-datepicker__month-year-dropdown-container--select,
.react-datepicker__year-dropdown-container--scroll,
.react-datepicker__month-dropdown-container--scroll,
.react-datepicker__month-year-dropdown-container--scroll {
display: inline-block;
margin: 0 2px;
}
.react-datepicker__current-month,
.react-datepicker-time__header,
.react-datepicker-year-header {
margin-top: 0;
font-weight: bold;
font-size: 0.944rem;
}
.react-datepicker-time__header {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.react-datepicker__navigation {
align-items: center;
background: none;
display: flex;
justify-content: center;
text-align: center;
cursor: pointer;
position: absolute;
top: 2px;
padding: 0;
border: none;
z-index: 1;
height: 32px;
width: 32px;
text-indent: -999em;
overflow: hidden;
}
.react-datepicker__navigation--previous {
left: 2px;
}
.react-datepicker__navigation--next {
right: 2px;
}
.react-datepicker__navigation--next--with-time:not(
.react-datepicker__navigation--next--with-today-button
) {
right: 85px;
}
.react-datepicker__navigation--years {
position: relative;
top: 0;
display: block;
margin-left: auto;
margin-right: auto;
}
.react-datepicker__navigation--years-previous {
top: 4px;
}
.react-datepicker__navigation--years-upcoming {
top: -4px;
}
.react-datepicker__navigation:hover *::before {
border-color: var(--lighter-gray);
}
.react-datepicker__navigation-icon {
position: relative;
top: -1px;
font-size: 20px;
width: 0;
}
.react-datepicker__navigation-icon--next {
left: -2px;
}
.react-datepicker__navigation-icon--next::before {
transform: rotate(45deg);
left: -7px;
}
.react-datepicker__navigation-icon--previous {
right: -2px;
}
.react-datepicker__navigation-icon--previous::before {
transform: rotate(225deg);
right: -7px;
}
.react-datepicker__month-container {
float: left;
}
.react-datepicker__year {
margin: 0.4rem;
text-align: center;
}
.react-datepicker__year-wrapper {
display: flex;
flex-wrap: wrap;
max-width: 180px;
}
.react-datepicker__year .react-datepicker__year-text {
display: inline-block;
width: 4rem;
margin: 2px;
}
.react-datepicker__month {
margin: 0.4rem;
text-align: center;
}
.react-datepicker__month .react-datepicker__month-text,
.react-datepicker__month .react-datepicker__quarter-text {
display: inline-block;
width: 4rem;
margin: 2px;
}
.react-datepicker__input-time-container {
clear: both;
width: 100%;
float: left;
margin: 5px 0 10px 15px;
text-align: left;
}
.react-datepicker__input-time-container .react-datepicker-time__caption {
display: inline-block;
}
.react-datepicker__input-time-container
.react-datepicker-time__input-container {
display: inline-block;
}
.react-datepicker__input-time-container
.react-datepicker-time__input-container
.react-datepicker-time__input {
display: inline-block;
margin-left: 10px;
}
.react-datepicker__input-time-container
.react-datepicker-time__input-container
.react-datepicker-time__input
input {
width: auto;
}
.react-datepicker__input-time-container
.react-datepicker-time__input-container
.react-datepicker-time__input
input[type="time"]::-webkit-inner-spin-button,
.react-datepicker__input-time-container
.react-datepicker-time__input-container
.react-datepicker-time__input
input[type="time"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.react-datepicker__input-time-container
.react-datepicker-time__input-container
.react-datepicker-time__input
input[type="time"] {
-moz-appearance: textfield;
}
.react-datepicker__input-time-container
.react-datepicker-time__input-container
.react-datepicker-time__delimiter {
margin-left: 5px;
display: inline-block;
}
.react-datepicker__day-names,
.react-datepicker__week {
white-space: nowrap;
}
.react-datepicker__day-names {
margin-bottom: -8px;
}
.react-datepicker__day-name,
.react-datepicker__day,
.react-datepicker__time-name {
color: var(--fg);
display: inline-block;
width: 1.7rem;
line-height: 1.7rem;
text-align: center;
margin: 0.166rem;
}
.react-datepicker__day,
.react-datepicker__month-text,
.react-datepicker__quarter-text,
.react-datepicker__year-text {
cursor: pointer;
}
.react-datepicker__day:hover,
.react-datepicker__month-text:hover,
.react-datepicker__quarter-text:hover,
.react-datepicker__year-text:hover {
border-radius: 0.3rem;
background-color: var(--light-gray);
}
.react-datepicker__day--today,
.react-datepicker__month-text--today,
.react-datepicker__quarter-text--today,
.react-datepicker__year-text--today {
font-weight: bold;
}
.react-datepicker__day--highlighted,
.react-datepicker__month-text--highlighted,
.react-datepicker__quarter-text--highlighted,
.react-datepicker__year-text--highlighted {
border-radius: 0.3rem;
background-color: #3dcc4a;
color: var(--fg);
}
.react-datepicker__day--highlighted:hover,
.react-datepicker__month-text--highlighted:hover,
.react-datepicker__quarter-text--highlighted:hover,
.react-datepicker__year-text--highlighted:hover {
background-color: #32be3f;
}
.react-datepicker__day--selected,
.react-datepicker__day--in-selecting-range,
.react-datepicker__day--in-range,
.react-datepicker__month-text--selected,
.react-datepicker__month-text--in-selecting-range,
.react-datepicker__month-text--in-range,
.react-datepicker__quarter-text--selected,
.react-datepicker__quarter-text--in-selecting-range,
.react-datepicker__quarter-text--in-range,
.react-datepicker__year-text--selected,
.react-datepicker__year-text--in-selecting-range,
.react-datepicker__year-text--in-range {
border-radius: 0.3rem;
background-color: var(--light-gray);
color: var(--fg);
}
.react-datepicker__day--selected:hover {
background-color: var(--gray);
}
.react-datepicker__day--keyboard-selected,
.react-datepicker__month-text--keyboard-selected,
.react-datepicker__quarter-text--keyboard-selected,
.react-datepicker__year-text--keyboard-selected {
border-radius: 0.3rem;
background-color: var(--light-gray);
color: var(--fg);
}
.react-datepicker__day--keyboard-selected:hover {
background-color: var(--gray);
}
.react-datepicker__month--selecting-range
.react-datepicker__day--in-range:not(
.react-datepicker__day--in-selecting-range,
.react-datepicker__month-text--in-selecting-range,
.react-datepicker__quarter-text--in-selecting-range,
.react-datepicker__year-text--in-selecting-range
) {
background-color: var(--bg);
color: var(--fg);
}
.react-datepicker {
transform: scale(1.15) translateY(-12px);
}
.react-datepicker__day--disabled {
color: var(--darker-gray);
}
.react-datepicker__day--disabled:hover {
background-color: transparent;
cursor: not-allowed;
}
.react-datepicker__aria-live {
position: absolute;
clip-path: circle(0);
border: 0;
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
width: 1px;
white-space: nowrap;
}

@ -1,6 +1,5 @@
import { getMetadata } from "src/app/lib/metadata"
import NewPost from "src/app/(drift)/(posts)/new/components/new"
import "./components/react-datepicker.css"
import { PageTitle } from "@components/page-title"
import { PageWrapper } from "@components/page-wrapper"

@ -30,7 +30,7 @@ const CreatedAgoBadge = ({ createdAt }: { createdAt: string | Date }) => {
return (
// TODO: investigate tooltip not showing
<Tooltip content={formattedTime}>
<Badge onClick={onClick} variant={"outline"}>
<Badge onClick={onClick} variant={"outline"} suppressHydrationWarning>
{" "}
<>{time}</>
</Badge>

@ -0,0 +1,64 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "react-feather"
import { DayPicker } from "react-day-picker"
import { cn } from "@lib/cn"
import { buttonVariants } from "@components/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
),
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "text-muted-foreground opacity-50",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames
}}
components={{
IconLeft: ({}) => <ChevronLeft className="w-4 h-4" />,
IconRight: ({}) => <ChevronRight className="w-4 h-4" />
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

@ -0,0 +1,46 @@
"use client"
import * as React from "react"
import { format } from "date-fns"
import { Calendar as CalendarIcon } from "react-feather"
import { cn } from "@lib/cn"
import { Button } from "@components/button"
import { Calendar } from "@components/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@components/popover"
export function DatePicker({
expiresAt,
setExpiresAt
}: {
expiresAt?: Date
setExpiresAt: React.Dispatch<React.SetStateAction<Date | undefined>>
}) {
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-[280px] justify-start text-left font-normal",
!expiresAt && "text-muted-foreground"
)}
>
<CalendarIcon className="w-4 h-4 mr-2" />
{expiresAt ? format(expiresAt, "PPP") : <span>Won't expire</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={expiresAt}
onSelect={(date) => {
setExpiresAt(date)
}}
initialFocus
fromDate={new Date()}
/>
</PopoverContent>
</Popover>
)
}

@ -115,6 +115,13 @@ export default function Header() {
<NavLink href="/settings" disabled={!isAuthenticated}>
Settings
</NavLink>
<span
aria-hidden
className="text-sm font-medium transition-colors cursor-pointer text-muted-foreground hover:text-primary"
onClick={toggleTheme}
>
Theme
</span>
{isAdmin && <NavLink href="/admin">Admin</NavLink>}
{isAuthenticated !== undefined && (
<>
@ -124,13 +131,6 @@ export default function Header() {
{isAuthenticated === false && (
<NavLink href="/signin">Sign In</NavLink>
)}
<span
aria-hidden
className="text-sm font-medium transition-colors cursor-pointer text-muted-foreground hover:text-primary"
onClick={toggleTheme}
>
<FadeIn>{resolvedTheme === "dark" ? "Light" : "Dark"}</FadeIn>
</span>
</>
)}
</ul>
@ -149,7 +149,7 @@ function NavLink({ href, disabled, children }: NavLinkProps) {
const baseClasses =
"text-sm text-muted-foreground font-medium transition-colors hover:text-primary"
const activeClasses = "text-primary border-primary"
const disabledClasses = "text-gray-400 hover:text-gray-400 cursor-default"
const disabledClasses = "text-gray-600 hover:text-gray-400 cursor-not-allowed"
const segments = useSelectedLayoutSegments()
const activeSegment = segments[segments.length - 1]

@ -11,12 +11,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, label, hideLabel, ...props }, ref) => {
const id = React.useId()
return (
<span className="flex flex-row w-full items-center">
<span className="flex flex-row items-center w-full">
{label && !hideLabel ? (
<label
htmlFor={id}
className={cn(
"h-10 text-sm font-medium text-muted-foreground border border-input bg-transparent px-3 py-2 rounded-md",
"h-10 rounded-md border border-input bg-transparent px-3 py-2 text-sm font-medium text-muted-foreground",
"rounded-br-none rounded-tr-none",
className
)}

@ -1,15 +1,16 @@
import NextLink from "next/link"
import styles from "./link.module.css"
import { cn } from "@lib/cn"
type LinkProps = {
colored?: boolean
children: React.ReactNode
} & React.ComponentProps<typeof NextLink>
const Link = ({ colored, children, ...props }: LinkProps) => {
const className = colored ? `${styles.link} ${styles.color}` : styles.link
const Link = ({ colored, className, children, ...props }: LinkProps) => {
const classes = colored ? "text-blue-500 no-underline" : "no-underline"
return (
<NextLink {...props} className={className}>
<NextLink {...props} className={cn(classes, className)}>
{children}
</NextLink>
)

@ -1,35 +1,31 @@
// largely from https://github.com/shadcn/taxonomy
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import clsx from "clsx"
import styles from "./popover.module.css"
type PopoverProps = PopoverPrimitive.PopoverProps
export function Popover({ ...props }: PopoverProps) {
return <PopoverPrimitive.Root {...props} />
}
Popover.Trigger = React.forwardRef<
HTMLButtonElement,
PopoverPrimitive.PopoverTriggerProps
>(function PopoverTrigger({ ...props }, ref) {
return <PopoverPrimitive.Trigger {...props} ref={ref} />
})
Popover.Portal = PopoverPrimitive.Portal
Popover.Content = React.forwardRef<
HTMLDivElement,
PopoverPrimitive.PopoverContentProps
>(function PopoverContent({ className, ...props }, ref) {
return (
<PopoverPrimitive.Content
ref={ref}
align="end"
className={clsx(styles.root, className)}
{...props}
/>
)
})
import { cn } from "@lib/cn"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none 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}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

@ -98,12 +98,12 @@ const PostList = ({
return (
<Stack className={styles.container} alignItems="center">
{!hideSearch && posts.length > 0 && (
{!hideSearch && (
<div className={styles.searchContainer}>
<Input
placeholder="Search..."
onChange={onSearchChange}
disabled={!posts}
disabled={!posts || posts.length === 0}
style={{ maxWidth: 300 }}
aria-label="Search"
value={searchValue}

@ -44,13 +44,6 @@
padding: var(--gap-quarter);
}
li a {
display: flex;
align-items: center;
gap: var(--gap);
color: var(--darker-gray);
}
li a:hover {
color: var(--link);
text-decoration: none;

@ -157,14 +157,11 @@ const ListItem = ({
{post?.files?.map(
(file: Pick<PostWithFiles, "files">["files"][0]) => {
return (
<li key={file.id}>
<li key={file.id} className="text-black">
<Link
colored
href={`/post/${post.id}#${file.title}`}
style={{
display: "flex",
alignItems: "center"
}}
className="flex items-center gap-2 font-mono text-sm text-foreground"
>
{getIconFromFilename(file.title)}
{file.title || "Untitled file"}

@ -1,4 +1,4 @@
import styles from "./skeleton.module.css"
import { cn } from "@lib/cn"
export default function Skeleton({
width = 100,
@ -13,8 +13,13 @@ export default function Skeleton({
}) {
return (
<div
className={styles.skeleton}
style={{ width, height, borderRadius, ...style }}
className={cn("animate-pulse bg-gray-300 dark:bg-gray-800")}
style={{
width,
height,
borderRadius,
...style
}}
/>
)
}

@ -1,4 +0,0 @@
.skeleton {
background-color: var(--lighter-gray);
border-radius: var(--radius);
}

@ -1,13 +1,13 @@
import { getSession } from "next-auth/react"
const protocol = process.env.NODE_ENV === "development" ? "http://" : "https://"
// const protocol = process.env.NODE_ENV === "development" ? "http://" : "https://"
/**
* a fetch wrapper that adds `userId={userId}` to the query string
*/
export async function fetchWithUser(url: string, options: RequestInit = {}) {
// TODO: figure out if this extra network call hurts performance
const session = await getSession()
const newUrl = new URL(url, `${protocol}${process.env.NEXT_PUBLIC_DRIFT_URL}`)
const newUrl = new URL(url, `${process.env.NEXT_PUBLIC_DRIFT_URL}`)
newUrl.searchParams.append("userId", session?.user.id || "")
return fetch(newUrl.toString(), options)
}

@ -1,5 +1,4 @@
import "@styles/globals.css"
import "@styles/markdown.css"
import Layout from "@components/layout"
import { Inter } from "next/font/google"

@ -12,7 +12,7 @@
--small-gap: 4rem;
--big-gap: 4rem;
--main-content: 55rem;
--main-content: min(55rem, 100vw);
--radius: 8px;
--inline-radius: 5px;

@ -49,7 +49,7 @@ article ul li.reset .check {
/* Checkbox */
input[type="checkbox"] {
article input[type="checkbox"] {
vertical-align: middle;
appearance: none;
display: inline-block;
@ -64,7 +64,7 @@ input[type="checkbox"] {
border-radius: 3px;
}
input[type="checkbox"]:checked {
article input[type="checkbox"]:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='black' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
border-color: transparent;
background-color: currentColor;
@ -73,18 +73,18 @@ input[type="checkbox"]:checked {
background-repeat: no-repeat;
}
html[data-theme="light"] input[type="checkbox"]:checked {
html[data-theme="light"] article input[type="checkbox"]:checked {
background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M5.707 7.293a1 1 0 0 0-1.414 1.414l2 2a1 1 0 0 0 1.414 0l4-4a1 1 0 0 0-1.414-1.414L7 8.586 5.707 7.293z'/%3e%3c/svg%3e");
}
input[type="checkbox"]:focus {
article input[type="checkbox"]:focus {
outline: none;
border-color: var(--fg);
}
/* Code Snippets */
.token-line:not(:last-child) {
article .token-line:not(:last-child) {
min-height: 1.4rem;
}
@ -107,16 +107,16 @@ article pre {
background-color: var(--lighter-gray);
}
table {
article table {
border-collapse: collapse;
}
table th {
article table th {
font-weight: 500;
}
table th,
table td {
article table th,
article table td {
padding: 0.35rem 0.75rem;
border: 1px solid var(--light-gray);
}

@ -1,76 +1,76 @@
const { fontFamily } = require("tailwindcss/defaultTheme")
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: ["src/app/**/*.{ts,tsx}"],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px"
}
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))"
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))"
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))"
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))"
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))"
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))"
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))"
}
},
borderRadius: {
lg: `var(--radius)`,
md: `calc(var(--radius) - 2px)`,
sm: "calc(var(--radius) - 4px)"
},
fontFamily: {
sans: ["var(--font-sans)", ...fontFamily.sans]
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" }
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 }
}
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out"
}
}
},
plugins: [require("tailwindcss-animate")]
}
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: 0 },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
Loading…
Cancel
Save