mirror of https://github.com/MaxLeiter/Drift
parent
702f59caf8
commit
5d5fd3182e
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: {
|
||||||
"postcss-nested",
|
"@tailwindcss/nesting": {},
|
||||||
"postcss-flexbugs-fixes",
|
tailwindcss: {},
|
||||||
"postcss-hover-media-feature",
|
autoprefixer: {}
|
||||||
[
|
}
|
||||||
"postcss-preset-env",
|
|
||||||
{
|
|
||||||
stage: 3,
|
|
||||||
features: {
|
|
||||||
"custom-media-queries": true,
|
|
||||||
"custom-properties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.listWrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list {
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list .formattingIcons {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
@ -1,60 +0,0 @@
|
|||||||
.root {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.listWrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.list {
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list .formattingIcons {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger {
|
|
||||||
width: 80px;
|
|
||||||
height: 30px;
|
|
||||||
margin: 4px 0;
|
|
||||||
font-family: inherit;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 15px;
|
|
||||||
line-height: 1;
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: color 0.1s ease;
|
|
||||||
margin-bottom: var(--gap-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger:hover {
|
|
||||||
background-color: var(--lighter-gray);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger:first-child {
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger:last-child {
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trigger[data-state="active"] {
|
|
||||||
color: var(--darkest-gray);
|
|
||||||
box-shadow: inset 0 -1px 0 0 currentColor, 0 1px 0 0 currentColor;
|
|
||||||
}
|
|
||||||
@ -1,372 +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;
|
|
||||||
}
|
|
||||||
@ -1,7 +1,15 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { PageTitle } from "@components/page-title"
|
||||||
|
import { PageWrapper } from "@components/page-wrapper"
|
||||||
import PostList from "@components/post-list"
|
import PostList from "@components/post-list"
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return <PostList skeleton={true} initialPosts={[]} />
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle>Your Posts</PageTitle>
|
||||||
|
<PageWrapper></PageWrapper>
|
||||||
|
<PostList skeleton={true} initialPosts={[]} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
export default function SettingsLayout({
|
|
||||||
children
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Settings</h1>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "var(--gap)",
|
|
||||||
marginBottom: "var(--gap)",
|
|
||||||
marginTop: "var(--gap)"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,5 +1,15 @@
|
|||||||
|
import { PageTitle } from "@components/page-title"
|
||||||
|
import { PageWrapper } from "@components/page-wrapper"
|
||||||
import SettingsGroup from "@components/settings-group"
|
import SettingsGroup from "@components/settings-group"
|
||||||
|
|
||||||
export default function SettingsLoading() {
|
export default function SettingsLoading() {
|
||||||
return <SettingsGroup skeleton />
|
return (
|
||||||
|
<>
|
||||||
|
<PageTitle>Settings</PageTitle>
|
||||||
|
<PageWrapper>
|
||||||
|
<SettingsGroup skeleton />
|
||||||
|
<SettingsGroup skeleton />
|
||||||
|
</PageWrapper>
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { authOptions } from "@lib/server/auth"
|
||||||
|
import NextAuth from "next-auth"
|
||||||
|
|
||||||
|
const handler = NextAuth(authOptions)
|
||||||
|
|
||||||
|
export { handler as GET, handler as POST }
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import config from "@lib/config"
|
||||||
|
import { NextRequest } from "next/server"
|
||||||
|
|
||||||
|
export const getRequiresPasscode = async () => {
|
||||||
|
const requiresPasscode = Boolean(config.registration_password)
|
||||||
|
return requiresPasscode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function GET(req: NextRequest) {
|
||||||
|
const searchParams = new URL(req.nextUrl).searchParams
|
||||||
|
const slug = searchParams.get("slug")
|
||||||
|
|
||||||
|
if (!slug || Array.isArray(slug)) {
|
||||||
|
return new Response(null, {
|
||||||
|
status: 400,
|
||||||
|
statusText: "Bad request"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slug === "requires-passcode") {
|
||||||
|
// return res.json({ requiresPasscode: await getRequiresPasscode() })
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ requiresPasscode: await getRequiresPasscode() }),
|
||||||
|
{
|
||||||
|
status: 200,
|
||||||
|
statusText: "OK",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ error: "Not found" }), {
|
||||||
|
status: 404,
|
||||||
|
statusText: "Not found",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
|
||||||
|
|
||||||
|
import { cn } from "@lib/cn"
|
||||||
|
import { buttonVariants } from "@components/button"
|
||||||
|
|
||||||
|
const AlertDialog = AlertDialogPrimitive.Root
|
||||||
|
|
||||||
|
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const AlertDialogPortal = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
|
||||||
|
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</AlertDialogPrimitive.Portal>
|
||||||
|
)
|
||||||
|
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
|
||||||
|
|
||||||
|
const AlertDialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-opacity animate-in fade-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const AlertDialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPortal>
|
||||||
|
<AlertDialogOverlay />
|
||||||
|
<AlertDialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed z-50 grid w-full max-w-lg scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</AlertDialogPortal>
|
||||||
|
))
|
||||||
|
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const AlertDialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-2 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogHeader.displayName = "AlertDialogHeader"
|
||||||
|
|
||||||
|
const AlertDialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
AlertDialogFooter.displayName = "AlertDialogFooter"
|
||||||
|
|
||||||
|
const AlertDialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const AlertDialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogDescription.displayName =
|
||||||
|
AlertDialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
const AlertDialogAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(buttonVariants(), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||||
|
|
||||||
|
const AlertDialogCancel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AlertDialogPrimitive.Cancel
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline" }),
|
||||||
|
"mt-2 sm:mt-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogTrigger,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogFooter,
|
||||||
|
AlertDialogTitle,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel
|
||||||
|
}
|
||||||
@ -1,21 +1,35 @@
|
|||||||
import React from "react"
|
import * as React from "react"
|
||||||
import styles from "./badge.module.css"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
type BadgeProps = {
|
import { cn } from "@lib/cn"
|
||||||
type: "primary" | "secondary" | "error" | "warning"
|
|
||||||
} & React.HTMLAttributes<HTMLDivElement>
|
|
||||||
|
|
||||||
const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
|
const badgeVariants = cva(
|
||||||
({ type, children, ...rest }: BadgeProps, ref) => {
|
"inline-flex items-center border rounded-full font-medium px-2.5 py-0.5 text-xs transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
return (
|
{
|
||||||
<div className={styles.container} {...rest}>
|
variants: {
|
||||||
<div className={`${styles.badge} ${styles[type]}`} ref={ref}>
|
variant: {
|
||||||
{children}
|
default:
|
||||||
</div>
|
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||||
</div>
|
secondary:
|
||||||
)
|
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||||
|
outline: "text-foreground"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Badge.displayName = "Badge"
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
export default Badge
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import Badge from "../badge"
|
import { Badge } from "../badge"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visibility: string
|
visibility: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const VisibilityBadge = ({ visibility }: Props) => {
|
const VisibilityBadge = ({ visibility }: Props) => {
|
||||||
return <Badge type={"primary"}>{visibility}</Badge>
|
return <Badge variant={"outline"}>{visibility}</Badge>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default VisibilityBadge
|
export default VisibilityBadge
|
||||||
|
|||||||
@ -1,56 +0,0 @@
|
|||||||
.button {
|
|
||||||
user-select: none;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button:hover,
|
|
||||||
.button:focus {
|
|
||||||
color: var(--fg);
|
|
||||||
background: var(--bg);
|
|
||||||
border: 1px solid var(--darker-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
background: var(--lighter-gray);
|
|
||||||
color: var(--gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.button[disabled]:hover,
|
|
||||||
.button[disabled]:focus {
|
|
||||||
border: 1px solid currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary {
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary {
|
|
||||||
background: var(--fg);
|
|
||||||
color: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
height: 1em;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconRight {
|
|
||||||
margin-left: var(--gap-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.iconLeft {
|
|
||||||
margin-right: var(--gap-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon svg {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
transform: scale(1.2) translateY(-0.05em);
|
|
||||||
}
|
|
||||||
@ -1,86 +1,65 @@
|
|||||||
import styles from "./button.module.css"
|
import * as React from "react"
|
||||||
import { forwardRef } from "react"
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
import clsx from "clsx"
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@lib/cn"
|
||||||
import { Spinner } from "@components/spinner"
|
import { Spinner } from "@components/spinner"
|
||||||
|
|
||||||
type Props = React.DetailedHTMLProps<
|
const buttonVariants = cva(
|
||||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-75 disabled:pointer-events-none ring-offset-background",
|
||||||
HTMLButtonElement
|
{
|
||||||
> & {
|
variants: {
|
||||||
children?: React.ReactNode
|
variant: {
|
||||||
buttonType?: "primary" | "secondary"
|
default: "bg-primary/80 text-primary-foreground/80 hover:bg-primary/70",
|
||||||
className?: string
|
destructive:
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
iconRight?: React.ReactNode
|
outline:
|
||||||
iconLeft?: React.ReactNode
|
"border border-input hover:bg-accent hover:text-accent-foreground",
|
||||||
height?: string | number
|
secondary:
|
||||||
width?: string | number
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
padding?: string | number
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
margin?: string | number
|
link: "underline-offset-4 hover:underline text-primary"
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 py-2 px-4",
|
||||||
|
sm: "h-9 px-3 rounded-md",
|
||||||
|
lg: "h-11 px-8 rounded-md"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
loading?: boolean
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react/display-name
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
const Button = forwardRef<HTMLButtonElement, Props>(
|
|
||||||
(
|
(
|
||||||
{
|
{ className, variant, size, loading, children, asChild = false, ...props },
|
||||||
children,
|
|
||||||
onClick,
|
|
||||||
className,
|
|
||||||
buttonType = "secondary",
|
|
||||||
disabled = false,
|
|
||||||
iconRight,
|
|
||||||
iconLeft,
|
|
||||||
height = 40,
|
|
||||||
width,
|
|
||||||
padding = 10,
|
|
||||||
margin,
|
|
||||||
loading,
|
|
||||||
style,
|
|
||||||
...props
|
|
||||||
},
|
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(styles.button, className, {
|
|
||||||
[styles.primary]: buttonType === "primary",
|
|
||||||
[styles.secondary]: buttonType === "secondary"
|
|
||||||
})}
|
|
||||||
disabled={disabled || loading}
|
|
||||||
onClick={onClick}
|
|
||||||
style={{ height, width, margin, padding, ...style }}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children && iconLeft && (
|
{loading ? <Spinner className="mr-2" /> : null}
|
||||||
<span className={clsx(styles.icon, styles.iconLeft)}>{iconLeft}</span>
|
{children}
|
||||||
)}
|
</Comp>
|
||||||
{!loading &&
|
|
||||||
(children ? (
|
|
||||||
children
|
|
||||||
) : (
|
|
||||||
<span className={styles.icon}>{iconLeft || iconRight}</span>
|
|
||||||
))}
|
|
||||||
{loading && (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Spinner />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{children && iconRight && (
|
|
||||||
<span className={clsx(styles.icon, styles.iconRight)}>
|
|
||||||
{iconRight}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export default Button
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
|
|||||||
@ -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="h-4 w-4" />,
|
||||||
|
IconRight: ({}) => <ChevronRight className="h-4 w-4" />
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Calendar.displayName = "Calendar"
|
||||||
|
|
||||||
|
export { Calendar }
|
||||||
@ -1,16 +1,78 @@
|
|||||||
import styles from "./card.module.css"
|
import * as React from "react"
|
||||||
|
import { cn } from "@lib/cn"
|
||||||
|
|
||||||
export default function Card({
|
const Card = React.forwardRef<
|
||||||
children,
|
HTMLDivElement,
|
||||||
className,
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
...props
|
>(({ className, ...props }, ref) => (
|
||||||
}: {
|
<div
|
||||||
children?: React.ReactNode
|
ref={ref}
|
||||||
className?: string
|
className={cn(
|
||||||
} & React.ComponentProps<"div">) {
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
return (
|
className
|
||||||
<div className={`${styles.card} ${className || ""}`} {...props}>
|
)}
|
||||||
<div className={styles.content}>{children}</div>
|
{...props}
|
||||||
</div>
|
/>
|
||||||
)
|
))
|
||||||
}
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(" flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
|
|||||||
@ -1,165 +0,0 @@
|
|||||||
/** Based on https://github.com/pacocoursey/cmdk **/
|
|
||||||
.cmdk[cmdk-root] {
|
|
||||||
overflow: hidden;
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
box-shadow: 0 0 0 1px var(--lighter-gray), 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
||||||
transition: transform 100ms ease;
|
|
||||||
border-radius: var(--radius);
|
|
||||||
|
|
||||||
.dark & {
|
|
||||||
background: rgba(22, 22, 22, 0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cmdk {
|
|
||||||
/* centered */
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
z-index: 999999;
|
|
||||||
/* size */
|
|
||||||
max-width: 640px;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
[cmdk-list] {
|
|
||||||
background: var(--bg);
|
|
||||||
height: 500px;
|
|
||||||
overflow: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-input] {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
width: 100%;
|
|
||||||
font-size: 17px;
|
|
||||||
padding: 8px 8px 16px 8px;
|
|
||||||
outline: none;
|
|
||||||
/* background: var(--lightest-gray); */
|
|
||||||
color: var(--fg);
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--gray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-badge] {
|
|
||||||
height: 20px;
|
|
||||||
background: var(--grayA3);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--grayA11);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 4px 0 4px 4px;
|
|
||||||
user-select: none;
|
|
||||||
text-transform: capitalize;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-item] {
|
|
||||||
content-visibility: auto;
|
|
||||||
|
|
||||||
cursor: pointer;
|
|
||||||
height: 48px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 0 16px;
|
|
||||||
color: var(--darker-gray);
|
|
||||||
user-select: none;
|
|
||||||
will-change: background, color;
|
|
||||||
transition: all 150ms ease;
|
|
||||||
transition-property: none;
|
|
||||||
background: var(--bg);
|
|
||||||
|
|
||||||
&[aria-selected="true"] {
|
|
||||||
background: var(--lightest-gray);
|
|
||||||
color: var(--fg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-disabled="true"] {
|
|
||||||
/* TODO: improve this */
|
|
||||||
color: var(--bg);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transition-property: background;
|
|
||||||
background: var(--bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
& + [cmdk-item] {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
svg {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-list] {
|
|
||||||
max-height: 400px;
|
|
||||||
overflow: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
transition: 100ms ease;
|
|
||||||
transition-property: height;
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-shortcuts] {
|
|
||||||
display: flex;
|
|
||||||
margin-left: auto;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
kbd {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
font-size: 12px;
|
|
||||||
min-width: 20px;
|
|
||||||
padding: var(--gap-half);
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: var(--fg);
|
|
||||||
background: var(--light-gray);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-separator] {
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--light-gray);
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
*:not([hidden]) + [cmdk-group] {
|
|
||||||
margin-top: var(--gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-group-heading] {
|
|
||||||
user-select: none;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--gray);
|
|
||||||
padding: 0 var(--gap);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--gap);
|
|
||||||
margin-top: var(--gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-empty] {
|
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
height: 48px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
color: var(--gray);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,163 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { DialogProps } from "@radix-ui/react-dialog"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { Search } from "react-feather"
|
||||||
|
|
||||||
|
import { cn } from "@lib/cn"
|
||||||
|
import { Dialog, DialogContent } from "@components/dialog"
|
||||||
|
|
||||||
|
const Command = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Command.displayName = CommandPrimitive.displayName
|
||||||
|
|
||||||
|
type CommandDialogProps = DialogProps
|
||||||
|
|
||||||
|
const CommandDialog = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Dialog>,
|
||||||
|
CommandDialogProps
|
||||||
|
>(({ children, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogContent className="overflow-hidden p-0 shadow-2xl">
|
||||||
|
<Command
|
||||||
|
ref={ref}
|
||||||
|
className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
CommandDialog.displayName = Dialog.displayName
|
||||||
|
|
||||||
|
const CommandInput = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||||
|
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||||
|
|
||||||
|
const CommandList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandList.displayName = CommandPrimitive.List.displayName
|
||||||
|
|
||||||
|
const CommandEmpty = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||||
|
>((props, ref) => (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
ref={ref}
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||||
|
|
||||||
|
const CommandGroup = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||||
|
|
||||||
|
const CommandSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 h-px bg-border", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
const CommandItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const CommandShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CommandShortcut.displayName = "CommandShortcut"
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator
|
||||||
|
}
|
||||||
@ -1,16 +1,3 @@
|
|||||||
body [cmdk-dialog] {
|
body [cmdk-dialog] {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 999999;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
/* backdrop-filter: blur(4px); */
|
|
||||||
transition: opacity 100ms ease;
|
|
||||||
pointer-events: none;
|
|
||||||
will-change: opacity;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
"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="mr-2 h-4 w-4" />
|
||||||
|
{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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,128 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "react-feather"
|
||||||
|
|
||||||
|
import { cn } from "@lib/cn"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: DialogPrimitive.DialogPortalProps) => (
|
||||||
|
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||||
|
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</DialogPrimitive.Portal>
|
||||||
|
)
|
||||||
|
DialogPortal.displayName = DialogPrimitive.Portal.displayName
|
||||||
|
|
||||||
|
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-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X 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,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription
|
||||||
|
}
|
||||||
@ -0,0 +1,200 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||||
|
import { Check, ChevronRight, Circle } from "react-feather"
|
||||||
|
|
||||||
|
import { cn } from "@lib/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-accent data-[state=open]:bg-accent",
|
||||||
|
inset && "pl-8",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight 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-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||||
|
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 border bg-popover p-1 text-popover-foreground shadow-md animate-in 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 cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
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-accent focus:text-accent-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>
|
||||||
|
<Check 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-accent focus:text-accent-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>
|
||||||
|
<Circle className="h-2 w-2 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-muted", 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
|
||||||
|
}
|
||||||
@ -1,3 +0,0 @@
|
|||||||
.active {
|
|
||||||
color: var(--fg) !important;
|
|
||||||
}
|
|
||||||
@ -1,16 +1,120 @@
|
|||||||
import styles from "./header.module.css"
|
"use client"
|
||||||
import { HeaderButtons } from "./buttons"
|
|
||||||
|
import Link from "next/link"
|
||||||
|
import { cn } from "@lib/cn"
|
||||||
|
|
||||||
|
import { useSessionSWR } from "@lib/use-session-swr"
|
||||||
|
import { PropsWithChildren, useEffect, useState } from "react"
|
||||||
|
import { useSelectedLayoutSegments } from "next/navigation"
|
||||||
|
import Image from "next/image"
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Moon, Sun } from "react-feather"
|
||||||
|
import FadeIn from "@components/fade-in"
|
||||||
import MobileHeader from "./mobile"
|
import MobileHeader from "./mobile"
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
|
const { isAdmin, isAuthenticated } = useSessionSWR()
|
||||||
|
const { resolvedTheme, setTheme } = useTheme()
|
||||||
|
const [isMounted, setIsMounted] = useState(false)
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className={styles.header}>
|
<header className="mt-4 flex h-16 items-center justify-start md:justify-between">
|
||||||
<div className={styles.tabs}>
|
<span className="hidden items-center md:flex">
|
||||||
<div className={styles.buttons}>
|
<Link href="/" className="mr-4 flex items-center">
|
||||||
<HeaderButtons />
|
<Image
|
||||||
</div>
|
src={"/assets/logo.svg"}
|
||||||
</div>
|
width={32}
|
||||||
<MobileHeader />
|
height={32}
|
||||||
|
alt=""
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
<span className="bg-transparent pl-4 text-lg font-bold">Drift</span>
|
||||||
|
</Link>
|
||||||
|
<nav className="flex space-x-4 lg:space-x-6">
|
||||||
|
<ul className="flex justify-center space-x-4">
|
||||||
|
<NavLink href="/home">Home</NavLink>
|
||||||
|
<NavLink href="/new" disabled={!isAuthenticated}>
|
||||||
|
New
|
||||||
|
</NavLink>
|
||||||
|
<NavLink href="/mine" disabled={!isAuthenticated}>
|
||||||
|
Yours
|
||||||
|
</NavLink>
|
||||||
|
<NavLink href="/settings" disabled={!isAuthenticated}>
|
||||||
|
Settings
|
||||||
|
</NavLink>
|
||||||
|
{isAdmin && <NavLink href="/admin">Admin</NavLink>}
|
||||||
|
{isAuthenticated !== undefined && (
|
||||||
|
<>
|
||||||
|
{isAuthenticated === true && (
|
||||||
|
<NavLink href="/signout">Sign Out</NavLink>
|
||||||
|
)}
|
||||||
|
{isAuthenticated === false && (
|
||||||
|
<NavLink href="/signin">Sign In</NavLink>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center justify-center md:hidden">
|
||||||
|
<MobileHeader />
|
||||||
|
</span>
|
||||||
|
{isMounted && (
|
||||||
|
<FadeIn>
|
||||||
|
<button
|
||||||
|
aria-hidden
|
||||||
|
className="ml-4 flex h-8 w-8 cursor-pointer items-center justify-center font-medium text-muted-foreground transition-colors hover:text-primary md:ml-0"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
title="Toggle theme"
|
||||||
|
>
|
||||||
|
{resolvedTheme === "dark" ? (
|
||||||
|
<Sun className="h-[16px] w-[16px]" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-[16px] w-[16px]" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</FadeIn>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NavLinkProps = PropsWithChildren<{
|
||||||
|
href: string
|
||||||
|
disabled?: boolean
|
||||||
|
active?: boolean
|
||||||
|
}>
|
||||||
|
|
||||||
|
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-600 hover:text-gray-400 cursor-not-allowed"
|
||||||
|
|
||||||
|
const segments = useSelectedLayoutSegments()
|
||||||
|
const activeSegment = segments[segments.length - 1]
|
||||||
|
const isActive =
|
||||||
|
activeSegment === href.slice(1) ||
|
||||||
|
// special case / because it's an alias of /home/page.tsx
|
||||||
|
(!activeSegment && href === "/home")
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={cn(
|
||||||
|
baseClasses,
|
||||||
|
isActive && activeClasses,
|
||||||
|
disabled && disabledClasses
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
.mobileTrigger {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 768px) {
|
|
||||||
.header {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wrapper [data-tab="github"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobileTrigger {
|
|
||||||
margin-top: var(--gap);
|
|
||||||
margin-bottom: var(--gap);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobileTrigger button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownItem a,
|
|
||||||
.dropdownItem button {
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
white-space: nowrap;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownItem:not(:first-child):not(:last-child) :global(button) {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownItem:first-child :global(button) {
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdownItem:last-child :global(button) {
|
|
||||||
border-top-left-radius: 0;
|
|
||||||
border-top-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,82 +1,51 @@
|
|||||||
import clsx from "clsx"
|
import * as React from "react"
|
||||||
import React from "react"
|
|
||||||
import styles from "./input.module.css"
|
|
||||||
|
|
||||||
type Props = React.HTMLProps<HTMLInputElement> & {
|
import { cn } from "@lib/cn"
|
||||||
|
|
||||||
|
export type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||||
label?: string
|
label?: string
|
||||||
width?: number | string
|
hideLabel?: boolean
|
||||||
height?: number | string
|
|
||||||
labelClassName?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// we have two special rules on top of the props:
|
|
||||||
// if onChange or value is passed, we require both, unless `disabled`
|
|
||||||
// if label is passed, we forbid aria-label and vice versa
|
|
||||||
type InputProps = Omit<Props, "onChange" | "value" | "label" | "aria-label"> &
|
|
||||||
(
|
|
||||||
| {
|
|
||||||
onChange: Props["onChange"]
|
|
||||||
value: Props["value"]
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
onChange?: never
|
|
||||||
value?: never
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
value: Props["value"]
|
|
||||||
disabled: true
|
|
||||||
onChange?: never
|
|
||||||
}
|
|
||||||
) &
|
|
||||||
(
|
|
||||||
| {
|
|
||||||
label: Props["label"]
|
|
||||||
"aria-label"?: never
|
|
||||||
} // if label is passed, we forbid aria-label and vice versa
|
|
||||||
| {
|
|
||||||
label?: never
|
|
||||||
"aria-label": Props["aria-label"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
(
|
({ className, type, label, hideLabel, ...props }, ref) => {
|
||||||
{ label, className, required, width, height, labelClassName, ...props },
|
const id = React.useId()
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const labelId = label?.replace(/\s/g, "-").toLowerCase()
|
|
||||||
return (
|
return (
|
||||||
<div
|
<span className="flex w-full flex-row items-center">
|
||||||
className={styles.wrapper}
|
{label && !hideLabel ? (
|
||||||
style={{
|
|
||||||
width,
|
|
||||||
height
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{label && (
|
|
||||||
<label
|
<label
|
||||||
htmlFor={labelId}
|
htmlFor={id}
|
||||||
className={clsx(styles.label, labelClassName)}
|
className={cn(
|
||||||
|
"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
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
)}
|
) : null}
|
||||||
|
{label && hideLabel ? (
|
||||||
|
<label htmlFor={id} className="sr-only">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
) : null}
|
||||||
<input
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
label && !hideLabel
|
||||||
|
? "rounded-bl-none rounded-tl-none border-l-0"
|
||||||
|
: "rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
id={labelId}
|
id={id}
|
||||||
className={clsx(styles.input, label && styles.withLabel, className)}
|
|
||||||
required={required}
|
|
||||||
{...props}
|
{...props}
|
||||||
style={{
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
...(props.style || {})
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Input.displayName = "Input"
|
Input.displayName = "Input"
|
||||||
|
|
||||||
export default Input
|
export { Input }
|
||||||
|
|||||||
@ -0,0 +1,128 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
|
||||||
|
import { cva } from "class-variance-authority"
|
||||||
|
import { ChevronDown } from "react-feather"
|
||||||
|
|
||||||
|
import { cn } from "@lib/cn"
|
||||||
|
|
||||||
|
const NavigationMenu = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-10 flex flex-1 items-center justify-center",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<NavigationMenuViewport />
|
||||||
|
</NavigationMenuPrimitive.Root>
|
||||||
|
))
|
||||||
|
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const NavigationMenuList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
|
||||||
|
|
||||||
|
const NavigationMenuItem = NavigationMenuPrimitive.Item
|
||||||
|
|
||||||
|
const navigationMenuTriggerStyle = cva(
|
||||||
|
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:bg-accent focus:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none bg-background hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent/50 data-[active]:bg-accent/50 h-10 py-2 px-4 group w-max"
|
||||||
|
)
|
||||||
|
|
||||||
|
const NavigationMenuTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}{" "}
|
||||||
|
<ChevronDown
|
||||||
|
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
</NavigationMenuPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const NavigationMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const NavigationMenuLink = NavigationMenuPrimitive.Link
|
||||||
|
|
||||||
|
const NavigationMenuViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||||
|
<NavigationMenuPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
NavigationMenuViewport.displayName =
|
||||||
|
NavigationMenuPrimitive.Viewport.displayName
|
||||||
|
|
||||||
|
const NavigationMenuIndicator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<NavigationMenuPrimitive.Indicator
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||||
|
</NavigationMenuPrimitive.Indicator>
|
||||||
|
))
|
||||||
|
NavigationMenuIndicator.displayName =
|
||||||
|
NavigationMenuPrimitive.Indicator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
navigationMenuTriggerStyle,
|
||||||
|
NavigationMenu,
|
||||||
|
NavigationMenuList,
|
||||||
|
NavigationMenuItem,
|
||||||
|
NavigationMenuContent,
|
||||||
|
NavigationMenuTrigger,
|
||||||
|
NavigationMenuLink,
|
||||||
|
NavigationMenuIndicator,
|
||||||
|
NavigationMenuViewport
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { cn } from "@lib/cn"
|
||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
export function PageTitle({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<React.HTMLProps<HTMLHeadingElement>>) {
|
||||||
|
return (
|
||||||
|
<h1 className={cn("pb-2 pt-2 text-4xl font-bold", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</h1>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { cn } from "@lib/cn"
|
||||||
|
import { PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
export function PageWrapper({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PropsWithChildren<React.HTMLProps<HTMLDivElement>>) {
|
||||||
|
return (
|
||||||
|
<div className={cn("mb-4 mt-4 flex flex-col gap-4", className)} {...props}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,35 +1,31 @@
|
|||||||
// largely from https://github.com/shadcn/taxonomy
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
import clsx from "clsx"
|
|
||||||
import styles from "./popover.module.css"
|
|
||||||
|
|
||||||
type PopoverProps = PopoverPrimitive.PopoverProps
|
import { cn } from "@lib/cn"
|
||||||
|
|
||||||
export function Popover({ ...props }: PopoverProps) {
|
const Popover = PopoverPrimitive.Root
|
||||||
return <PopoverPrimitive.Root {...props} />
|
|
||||||
}
|
|
||||||
|
|
||||||
Popover.Trigger = React.forwardRef<
|
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||||
HTMLButtonElement,
|
|
||||||
PopoverPrimitive.PopoverTriggerProps
|
|
||||||
>(function PopoverTrigger({ ...props }, ref) {
|
|
||||||
return <PopoverPrimitive.Trigger {...props} ref={ref} />
|
|
||||||
})
|
|
||||||
|
|
||||||
Popover.Portal = PopoverPrimitive.Portal
|
const PopoverContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||||
Popover.Content = React.forwardRef<
|
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||||
HTMLDivElement,
|
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||||
PopoverPrimitive.PopoverContentProps
|
<PopoverPrimitive.Portal>
|
||||||
>(function PopoverContent({ className, ...props }, ref) {
|
|
||||||
return (
|
|
||||||
<PopoverPrimitive.Content
|
<PopoverPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
align="end"
|
align={align}
|
||||||
className={clsx(styles.root, className)}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
</PopoverPrimitive.Portal>
|
||||||
})
|
))
|
||||||
|
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent }
|
||||||
|
|||||||
@ -1,24 +1,28 @@
|
|||||||
import styles from "./list-item.module.css"
|
import styles from "./list-item.module.css"
|
||||||
import Card from "@components/card"
|
import { Card, CardContent, CardHeader } from "@components/card"
|
||||||
import Skeleton from "@components/skeleton"
|
import Skeleton from "@components/skeleton"
|
||||||
|
|
||||||
export const ListItemSkeleton = () => (
|
export const ListItemSkeleton = () => (
|
||||||
<li>
|
<li>
|
||||||
<Card style={{ overflowY: "scroll" }}>
|
<Card style={{ overflowY: "scroll" }}>
|
||||||
{/* TODO: this is a bad way to do skeletons and is onlya ccurate on desktop */}
|
{/* TODO: this is a bad way to do skeletons and is only accurate on desktop */}
|
||||||
<div style={{ display: "flex", gap: 16, marginBottom: 14 }}>
|
<CardHeader>
|
||||||
<div className={styles.title}>
|
<div style={{ display: "flex", gap: 16, marginBottom: 14 }}>
|
||||||
{/* title */}
|
<div className={styles.title}>
|
||||||
<Skeleton width={80} height={32} />
|
{/* title */}
|
||||||
</div>
|
<Skeleton width={80} height={32} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.badges}>
|
<div className={styles.badges}>
|
||||||
<Skeleton width={60} height={32} />
|
<Skeleton width={60} height={32} />
|
||||||
<Skeleton width={60} height={32} />
|
<Skeleton width={60} height={32} />
|
||||||
<Skeleton width={60} height={32} />
|
<Skeleton width={60} height={32} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardHeader>
|
||||||
<Skeleton width={100} height={32} />
|
<CardContent>
|
||||||
|
<Skeleton width={100} height={32} />
|
||||||
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</li>
|
</li>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
.skeleton {
|
|
||||||
background-color: var(--lighter-gray);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
}
|
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
import { cn } from "@lib/cn"
|
||||||
import styles from "./spinner.module.css"
|
import styles from "./spinner.module.css"
|
||||||
|
|
||||||
export const Spinner = () => <div className={styles.spinner} />
|
export const Spinner = ({ className }: { className?: string }) => (
|
||||||
|
<div className={cn(styles.spinner, className)} />
|
||||||
|
)
|
||||||
|
|||||||
@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@lib/cn"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-transparent p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:border-b-2 data-[state=active]:border-foreground data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue