Merge branch 'main' into resolve-market-v2-final

This commit is contained in:
Marshall Polaris 2022-06-29 16:24:28 -07:00
commit 6175336af0
26 changed files with 484 additions and 340 deletions

View File

@ -48,7 +48,7 @@ jobs:
- name: Run Typescript checker on web client
if: ${{ success() || failure() }}
working-directory: web
run: tsc -b -v --pretty
run: tsc --pretty --project tsconfig.json --noEmit
- name: Run Typescript checker on cloud functions
if: ${{ success() || failure() }}
working-directory: functions

View File

@ -516,6 +516,22 @@ The American Civil Liberties Union is our nation's guardian of liberty, working
The U.S. Constitution and the Bill of Rights trumpet our aspirations for the kind of society that we want to be. But for much of our history, our nation failed to fulfill the promise of liberty for whole groups of people.`,
},
{
name: 'The Center for Election Science',
website: 'https://electionscience.org/',
photo: 'https://i.imgur.com/WvdHHZa.png',
preview:
'The Center for Election Science is a nonpartisan nonprofit dedicated to empowering voters with voting methods that strengthen democracy. We believe you deserve a vote that empowers you to impact the world you live in.',
description: `Founded in 2011, The Center for Election Science is a national, nonpartisan nonprofit focused on voting reform.
Our Mission To empower people with voting methods that strengthen democracy.
Our Vision A world where democracies thrive because voters voices are heard.
With an emphasis on approval voting, we bring better elections to people across the country through both advocacy and research.
The movement for a better way to vote is rapidly gaining momentum as voters grow tired of election results that dont represent the will of the people. In 2018, we worked with locals in Fargo, ND to help them become the first city in the U.S. to adopt approval voting. And in 2020, we helped grassroots activists empower the 300k people of St. Louis, MO with stronger democracy through approval voting.`,
},
].map((charity) => {
const slug = charity.name.toLowerCase().replace(/\s/g, '-')
return {

View File

@ -3,7 +3,8 @@
"version": "1.0.0",
"private": true,
"scripts": {
"verify": "(cd .. && yarn verify)"
"verify": "(cd .. && yarn verify)",
"verify:dir": "npx eslint . --max-warnings 0"
},
"sideEffects": false,
"dependencies": {

View File

@ -17,7 +17,8 @@
"db:backup-local": "firebase emulators:export --force ./firestore_export",
"db:rename-remote-backup-folder": "gsutil mv gs://$npm_package_config_firestore/firestore_export gs://$npm_package_config_firestore/firestore_export_$(date +%d-%m-%Y-%H-%M)",
"db:backup-remote": "yarn db:rename-remote-backup-folder && gcloud firestore export gs://$npm_package_config_firestore/firestore_export/",
"verify": "(cd .. && yarn verify)"
"verify": "(cd .. && yarn verify)",
"verify:dir": "npx eslint . --max-warnings 0; tsc -b -v --pretty"
},
"main": "functions/src/index.js",
"dependencies": {

View File

@ -8,7 +8,7 @@
"web"
],
"scripts": {
"verify": "(cd web && npx prettier --check .; yarn lint --max-warnings 0; tsc --pretty --project tsconfig.json --noEmit); (cd common && npx eslint . --max-warnings 0); (cd functions && npx eslint . --max-warnings 0; tsc --pretty --project tsconfig.json --noEmit)"
"verify": "(cd web && yarn verify:dir); (cd functions && yarn verify:dir)"
},
"dependencies": {},
"devDependencies": {

View File

@ -22,7 +22,7 @@ export function GroupSelector(props: {
const [isCreatingNewGroup, setIsCreatingNewGroup] = useState(false)
const [query, setQuery] = useState('')
const memberGroups = useMemberGroups(creator)
const memberGroups = useMemberGroups(creator?.id)
const filteredGroups = memberGroups
? query === ''
? memberGroups

View File

@ -0,0 +1,144 @@
import clsx from 'clsx'
import { User } from 'common/user'
import { useState } from 'react'
import { useUser } from 'web/hooks/use-user'
import { withTracking } from 'web/lib/service/analytics'
import { Row } from 'web/components/layout/row'
import { useMemberGroups } from 'web/hooks/use-group'
import { TextButton } from 'web/components/text-button'
import { Group } from 'common/group'
import { Modal } from 'web/components/layout/modal'
import { Col } from 'web/components/layout/col'
import { joinGroup, leaveGroup } from 'web/lib/firebase/groups'
import { firebaseLogin } from 'web/lib/firebase/users'
import { GroupLink } from 'web/pages/groups'
export function GroupsButton(props: { user: User }) {
const { user } = props
const [isOpen, setIsOpen] = useState(false)
const groups = useMemberGroups(user.id)
return (
<>
<TextButton onClick={() => setIsOpen(true)}>
<span className="font-semibold">{groups?.length ?? ''}</span> Groups
</TextButton>
<GroupsDialog
user={user}
groups={groups ?? []}
isOpen={isOpen}
setIsOpen={setIsOpen}
/>
</>
)
}
function GroupsDialog(props: {
user: User
groups: Group[]
isOpen: boolean
setIsOpen: (isOpen: boolean) => void
}) {
const { user, groups, isOpen, setIsOpen } = props
return (
<Modal open={isOpen} setOpen={setIsOpen}>
<Col className="rounded bg-white p-6">
<div className="p-2 pb-1 text-xl">{user.name}</div>
<div className="p-2 pt-0 text-sm text-gray-500">@{user.username}</div>
<GroupsList groups={groups} />
</Col>
</Modal>
)
}
function GroupsList(props: { groups: Group[] }) {
const { groups } = props
return (
<Col className="gap-2">
{groups.length === 0 && (
<div className="text-gray-500">No groups yet...</div>
)}
{groups
.sort((group1, group2) => group2.createdTime - group1.createdTime)
.map((group) => (
<GroupItem key={group.id} group={group} />
))}
</Col>
)
}
function GroupItem(props: { group: Group; className?: string }) {
const { group, className } = props
return (
<Row className={clsx('items-center justify-between gap-2 p-2', className)}>
<Row className="line-clamp-1 items-center gap-2">
<GroupLink group={group} />
</Row>
<JoinOrLeaveGroupButton group={group} />
</Row>
)
}
export function JoinOrLeaveGroupButton(props: {
group: Group
small?: boolean
className?: string
}) {
const { group, small, className } = props
const currentUser = useUser()
const isFollowing = currentUser
? group.memberIds.includes(currentUser.id)
: false
const onJoinGroup = () => {
if (!currentUser) return
joinGroup(group, currentUser.id)
}
const onLeaveGroup = () => {
if (!currentUser) return
leaveGroup(group, currentUser.id)
}
const smallStyle =
'btn !btn-xs border-2 border-gray-500 bg-white normal-case text-gray-500 hover:border-gray-500 hover:bg-white hover:text-gray-500'
if (!currentUser || isFollowing === undefined) {
if (!group.anyoneCanJoin)
return <div className={clsx(className, 'text-gray-500')}>Closed</div>
return (
<button
onClick={firebaseLogin}
className={clsx('btn btn-sm', small && smallStyle, className)}
>
Login to Join
</button>
)
}
if (isFollowing) {
return (
<button
className={clsx(
'btn btn-outline btn-sm',
small && smallStyle,
className
)}
onClick={withTracking(onLeaveGroup, 'leave group')}
>
Leave
</button>
)
}
if (!group.anyoneCanJoin)
return <div className={clsx(className, 'text-gray-500')}>Closed</div>
return (
<button
className={clsx('btn btn-sm', small && smallStyle, className)}
onClick={withTracking(onJoinGroup, 'join group')}
>
Join
</button>
)
}

View File

@ -3,7 +3,6 @@ import Link from 'next/link'
import {
HomeIcon,
MenuAlt3Icon,
PresentationChartLineIcon,
SearchIcon,
XIcon,
} from '@heroicons/react/outline'
@ -19,14 +18,9 @@ import NotificationsIcon from 'web/components/notifications-icon'
import { useIsIframe } from 'web/hooks/use-is-iframe'
import { trackCallback } from 'web/lib/service/analytics'
function getNavigation(username: string) {
function getNavigation() {
return [
{ name: 'Home', href: '/home', icon: HomeIcon },
{
name: 'Portfolio',
href: `/${username}?tab=bets`,
icon: PresentationChartLineIcon,
},
{
name: 'Notifications',
href: `/notifications`,
@ -55,38 +49,39 @@ export function BottomNavBar() {
}
const navigationOptions =
user === null
? signedOutNavigation
: getNavigation(user?.username || 'error')
user === null ? signedOutNavigation : getNavigation()
return (
<nav className="fixed inset-x-0 bottom-0 z-20 flex justify-between border-t-2 bg-white text-xs text-gray-700 lg:hidden">
{navigationOptions.map((item) => (
<NavBarItem key={item.name} item={item} currentPage={currentPage} />
))}
{user && (
<NavBarItem
key={'profile'}
currentPage={currentPage}
item={{
name: formatMoney(user.balance),
href: `/${user.username}?tab=bets`,
icon: () => (
<Avatar
className="mx-auto my-1"
size="xs"
username={user.username}
avatarUrl={user.avatarUrl}
noLink
/>
),
}}
/>
)}
<div
className="w-full select-none py-1 px-3 text-center hover:cursor-pointer hover:bg-indigo-200 hover:text-indigo-700"
onClick={() => setSidebarOpen(true)}
>
{user === null ? (
<>
<MenuAlt3Icon className="my-1 mx-auto h-6 w-6" aria-hidden="true" />
More
</>
) : user ? (
<>
<Avatar
className="mx-auto my-1"
size="xs"
username={user.username}
avatarUrl={user.avatarUrl}
noLink
/>
{formatMoney(user.balance)}
</>
) : (
<></>
)}
<MenuAlt3Icon className="my-1 mx-auto h-6 w-6" aria-hidden="true" />
More
</div>
<MobileSidebar
@ -109,7 +104,7 @@ function NavBarItem(props: { item: Item; currentPage: string }) {
)}
onClick={trackCallback('navbar: ' + item.name)}
>
<item.icon className="my-1 mx-auto h-6 w-6" />
{item.icon && <item.icon className="my-1 mx-auto h-6 w-6" />}
{item.name}
</a>
</Link>

View File

@ -8,7 +8,7 @@ import { trackCallback } from 'web/lib/service/analytics'
export function ProfileSummary(props: { user: User }) {
const { user } = props
return (
<Link href={`/${user.username}`}>
<Link href={`/${user.username}?tab=bets`}>
<a
onClick={trackCallback('sidebar: profile')}
className="group flex flex-row items-center gap-4 rounded-md py-3 text-gray-500 hover:bg-gray-100 hover:text-gray-700"

View File

@ -5,10 +5,9 @@ import {
DotsHorizontalIcon,
CashIcon,
HeartIcon,
PresentationChartLineIcon,
UserGroupIcon,
ChevronDownIcon,
TrendingUpIcon,
ChatIcon,
} from '@heroicons/react/outline'
import clsx from 'clsx'
import Link from 'next/link'
@ -26,15 +25,11 @@ import { useMemberGroups } from 'web/hooks/use-group'
import { groupPath } from 'web/lib/firebase/groups'
import { trackCallback, withTracking } from 'web/lib/service/analytics'
import { Group } from 'common/group'
import { Spacer } from '../layout/spacer'
function getNavigation(username: string) {
function getNavigation() {
return [
{ name: 'Home', href: '/home', icon: HomeIcon },
{
name: 'Portfolio',
href: `/${username}?tab=bets`,
icon: PresentationChartLineIcon,
},
{
name: 'Notifications',
href: `/notifications`,
@ -63,12 +58,10 @@ function getMoreNavigation(user?: User | null) {
}
return [
{ name: 'Send M$', href: '/links' },
{ name: 'Leaderboards', href: '/leaderboards' },
{ name: 'Charity', href: '/charity' },
{ name: 'Blog', href: 'https://news.manifold.markets' },
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
{ name: 'Twitter', href: 'https://twitter.com/ManifoldMarkets' },
{ name: 'Statistics', href: '/stats' },
{ name: 'About', href: 'https://docs.manifold.markets/$how-to' },
{
name: 'Sign out',
@ -90,8 +83,20 @@ const signedOutNavigation = [
]
const signedOutMobileNavigation = [
{
name: 'About',
href: 'https://docs.manifold.markets/$how-to',
icon: BookOpenIcon,
},
{ name: 'Charity', href: '/charity', icon: HeartIcon },
{ name: 'Leaderboards', href: '/leaderboards', icon: TrendingUpIcon },
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh', icon: ChatIcon },
]
const signedInMobileNavigation = [
...(IS_PRIVATE_MANIFOLD
? []
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
{
name: 'About',
href: 'https://docs.manifold.markets/$how-to',
@ -99,17 +104,24 @@ const signedOutMobileNavigation = [
},
]
const signedInMobileNavigation = [
...(IS_PRIVATE_MANIFOLD
? []
: [{ name: 'Get M$', href: '/add-funds', icon: CashIcon }]),
...signedOutMobileNavigation,
]
function getMoreMobileNav() {
return [
{ name: 'Send M$', href: '/links' },
{ name: 'Charity', href: '/charity' },
{ name: 'Discord', href: 'https://discord.gg/eHQBNBqXuh' },
{ name: 'Leaderboards', href: '/leaderboards' },
{
name: 'Sign out',
href: '#',
onClick: withTracking(firebaseLogout, 'sign out'),
},
]
}
export type Item = {
name: string
href: string
icon: React.ComponentType<{ className?: string }>
icon?: React.ComponentType<{ className?: string }>
}
function SidebarItem(props: { item: Item; currentPage: string }) {
@ -126,15 +138,17 @@ function SidebarItem(props: { item: Item; currentPage: string }) {
)}
aria-current={item.href == currentPage ? 'page' : undefined}
>
<item.icon
className={clsx(
item.href == currentPage
? 'text-gray-500'
: 'text-gray-400 group-hover:text-gray-500',
'-ml-1 mr-3 h-6 w-6 flex-shrink-0'
)}
aria-hidden="true"
/>
{item.icon && (
<item.icon
className={clsx(
item.href == currentPage
? 'text-gray-500'
: 'text-gray-400 group-hover:text-gray-500',
'-ml-1 mr-3 h-6 w-6 flex-shrink-0'
)}
aria-hidden="true"
/>
)}
<span className="truncate">{item.name}</span>
</a>
</Link>
@ -163,27 +177,17 @@ function MoreButton() {
return <SidebarButton text={'More'} icon={DotsHorizontalIcon} />
}
function GroupsButton() {
return (
<SidebarButton icon={UserGroupIcon} text={'Groups'}>
<ChevronDownIcon className=" mt-0.5 ml-2 h-5 w-5" aria-hidden="true" />
</SidebarButton>
)
}
export default function Sidebar(props: { className?: string }) {
const { className } = props
const router = useRouter()
const currentPage = router.pathname
const user = useUser()
const navigationOptions = !user
? signedOutNavigation
: getNavigation(user?.username || 'error')
const navigationOptions = !user ? signedOutNavigation : getNavigation()
const mobileNavigationOptions = !user
? signedOutMobileNavigation
: signedInMobileNavigation
const memberItems = (useMemberGroups(user) ?? []).map((group: Group) => ({
const memberItems = (useMemberGroups(user?.id) ?? []).map((group: Group) => ({
name: group.name,
href: groupPath(group.slug),
}))
@ -191,95 +195,73 @@ export default function Sidebar(props: { className?: string }) {
return (
<nav aria-label="Sidebar" className={className}>
<ManifoldLogo className="pb-6" twoLine />
<CreateQuestionButton user={user} />
<Spacer h={4} />
{user && (
<div className="mb-2" style={{ minHeight: 80 }}>
<div className="w-full" style={{ minHeight: 80 }}>
<ProfileSummary user={user} />
</div>
)}
{/* Mobile navigation */}
<div className="space-y-1 lg:hidden">
{user && (
<MenuButton
buttonContent={<GroupsButton />}
menuItems={[{ name: 'Explore', href: '/groups' }, ...memberItems]}
className={'relative z-50 flex-shrink-0'}
/>
)}
{mobileNavigationOptions.map((item) => (
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
))}
{!user && (
<SidebarItem
key={'Groups'}
item={{ name: 'Groups', href: '/groups', icon: UserGroupIcon }}
currentPage={currentPage}
/>
)}
{user && (
<MenuButton
menuItems={[
{
name: 'Blog',
href: 'https://news.manifold.markets',
},
{
name: 'Discord',
href: 'https://discord.gg/eHQBNBqXuh',
},
{
name: 'Twitter',
href: 'https://twitter.com/ManifoldMarkets',
},
{
name: 'Statistics',
href: '/stats',
},
{
name: 'Sign out',
href: '#',
onClick: withTracking(firebaseLogout, 'sign out'),
},
]}
menuItems={getMoreMobileNav()}
buttonContent={<MoreButton />}
/>
)}
<GroupsList currentPage={currentPage} memberItems={memberItems} />
</div>
{/* Desktop navigation */}
<div className="hidden space-y-1 lg:block">
{navigationOptions.map((item) =>
item.name === 'Notifications' ? (
<div key={item.href}>
<SidebarItem item={item} currentPage={currentPage} />
{user && (
<MenuButton
key={'groupsdropdown'}
buttonContent={<GroupsButton />}
menuItems={[
{ name: 'Explore', href: '/groups' },
...memberItems,
]}
className={'relative z-50 flex-shrink-0'}
/>
)}
</div>
) : (
<SidebarItem
key={item.href}
item={item}
currentPage={currentPage}
/>
)
)}
{navigationOptions.map((item) => (
<SidebarItem key={item.href} item={item} currentPage={currentPage} />
))}
<MenuButton
menuItems={getMoreNavigation(user)}
buttonContent={<MoreButton />}
/>
{/* Spacer if there are any groups */}
{memberItems.length > 0 && (
<div className="py-3">
<div className="h-[1px] bg-gray-300" />
</div>
)}
<GroupsList currentPage={currentPage} memberItems={memberItems} />
</div>
<CreateQuestionButton user={user} />
</nav>
)
}
function GroupsList(props: { currentPage: string; memberItems: Item[] }) {
const { currentPage, memberItems } = props
return (
<>
<SidebarItem
item={{ name: 'Groups', href: '/groups', icon: UserGroupIcon }}
currentPage={currentPage}
/>
<div className="mt-1 space-y-0.5">
{memberItems.map((item) => (
<a
key={item.name}
href={item.href}
className="group flex items-center rounded-md px-3 py-2 text-sm font-medium text-gray-600 hover:bg-gray-100 hover:text-gray-900"
>
<span className="truncate">&nbsp; {item.name}</span>
</a>
))}
</div>
</>
)
}

View File

@ -7,8 +7,8 @@ import clsx from 'clsx'
import { Comment } from 'common/comment'
import { User } from 'common/user'
import { formatMoney } from 'common/util/format'
import { debounce, sumBy } from 'lodash'
import { useEffect, useMemo, useRef, useState } from 'react'
import { debounce, sum } from 'lodash'
import { useEffect, useRef, useState } from 'react'
import { CommentTips } from 'web/hooks/use-tip-txns'
import { useUser } from 'web/hooks/use-user'
import { transact } from 'web/lib/firebase/fn-call'
@ -16,33 +16,24 @@ import { track } from 'web/lib/service/analytics'
import { Row } from './layout/row'
import { Tooltip } from './tooltip'
// xth triangle number * 5 = 5 + 10 + 15 + ... + (x * 5)
const quad = (x: number) => (5 / 2) * x * (x + 1)
// inverse (see https://math.stackexchange.com/questions/2041988/how-to-get-inverse-of-formula-for-sum-of-integers-from-1-to-nsee )
const invQuad = (y: number) => Math.sqrt((2 / 5) * y + 1 / 4) - 1 / 2
export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
const { comment, tips } = prop
const me = useUser()
const myId = me?.id ?? ''
const savedTip = tips[myId] as number | undefined
const savedTip = tips[myId] ?? 0
// optimistically increase the tip count, but debounce the update
const [localTip, setLocalTip] = useState(savedTip ?? 0)
const [localTip, setLocalTip] = useState(savedTip)
// listen for user being set
const initialized = useRef(false)
useEffect(() => {
if (savedTip && !initialized.current) {
setLocalTip(savedTip)
if (tips[myId] && !initialized.current) {
setLocalTip(tips[myId])
initialized.current = true
}
}, [savedTip])
}, [tips, myId])
const score = useMemo(() => {
const tipVals = Object.values({ ...tips, [myId]: localTip })
return sumBy(tipVals, invQuad)
}, [localTip, tips, myId])
const total = sum(Object.values(tips)) - savedTip + localTip
// declare debounced function only on first render
const [saveTip] = useState(() =>
@ -80,7 +71,7 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
const changeTip = (tip: number) => {
setLocalTip(tip)
me && saveTip(me, tip - (savedTip ?? 0))
me && saveTip(me, tip - savedTip)
}
return (
@ -88,13 +79,13 @@ export function Tipper(prop: { comment: Comment; tips: CommentTips }) {
<DownTip
value={localTip}
onChange={changeTip}
disabled={!me || localTip <= 0}
disabled={!me || localTip <= savedTip}
/>
<span className="font-bold">{Math.floor(score)} </span>
<span className="font-bold">{Math.floor(total)}</span>
<UpTip
value={localTip}
onChange={changeTip}
disabled={!me || me.id === comment.userId}
disabled={!me || me.id === comment.userId || me.balance < localTip + 5}
/>
{localTip === 0 ? (
''
@ -118,16 +109,15 @@ function DownTip(prop: {
disabled?: boolean
}) {
const { onChange, value, disabled } = prop
const marginal = 5 * invQuad(value)
return (
<Tooltip
className="tooltip-bottom"
text={!disabled && `Refund ${formatMoney(marginal)}`}
text={!disabled && `-${formatMoney(5)}`}
>
<button
className="flex h-max items-center hover:text-red-600 disabled:text-gray-300"
disabled={disabled}
onClick={() => onChange(value - marginal)}
onClick={() => onChange(value - 5)}
>
<ChevronLeftIcon className="h-6 w-6" />
</button>
@ -141,19 +131,18 @@ function UpTip(prop: {
disabled?: boolean
}) {
const { onChange, value, disabled } = prop
const marginal = 5 * invQuad(value) + 5
return (
<Tooltip
className="tooltip-bottom"
text={!disabled && `Tip ${formatMoney(marginal)}`}
text={!disabled && `Tip ${formatMoney(5)}`}
>
<button
className="hover:text-primary flex h-max items-center disabled:text-gray-300"
disabled={disabled}
onClick={() => onChange(value + marginal)}
onClick={() => onChange(value + 5)}
>
{value >= quad(2) ? (
{value >= 10 ? (
<ChevronDoubleRightIcon className="text-primary mx-1 h-6 w-6" />
) : value > 0 ? (
<ChevronRightIcon className="text-primary h-6 w-6" />

View File

@ -36,6 +36,7 @@ import { FollowersButton, FollowingButton } from './following-button'
import { useFollows } from 'web/hooks/use-follows'
import { FollowButton } from './follow-button'
import { PortfolioMetrics } from 'common/user'
import { GroupsButton } from 'web/components/groups/groups-button'
export function UserLink(props: {
name: string
@ -197,6 +198,7 @@ export function UserPage(props: {
<Row className="gap-4">
<FollowingButton user={user} />
<FollowersButton user={user} />
<GroupsButton user={user} />
</Row>
{user.website && (

View File

@ -2,16 +2,16 @@ import { useEffect } from 'react'
import { useFirestoreDocumentData } from '@react-query-firebase/firestore'
import {
Contract,
contractDocRef,
contracts,
listenForContract,
} from 'web/lib/firebase/contracts'
import { useStateCheckEquality } from './use-state-check-equality'
import { DocumentData } from 'firebase/firestore'
import { doc, DocumentData } from 'firebase/firestore'
export const useContract = (contractId: string) => {
const result = useFirestoreDocumentData<DocumentData, Contract>(
['contracts', contractId],
contractDocRef(contractId),
doc(contracts, contractId),
{ subscribe: true, includeMetadataChanges: true }
)

View File

@ -29,11 +29,11 @@ export const useGroups = () => {
return groups
}
export const useMemberGroups = (user: User | null | undefined) => {
export const useMemberGroups = (userId: string | null | undefined) => {
const [memberGroups, setMemberGroups] = useState<Group[] | undefined>()
useEffect(() => {
if (user) return listenForMemberGroups(user.id, setMemberGroups)
}, [user])
if (userId) return listenForMemberGroups(userId, setMemberGroups)
}, [userId])
return memberGroups
}

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { useFirestoreDocumentData } from '@react-query-firebase/firestore'
import { QueryClient } from 'react-query'
import { DocumentData } from 'firebase/firestore'
import { doc, DocumentData } from 'firebase/firestore'
import { PrivateUser } from 'common/user'
import {
getUser,
@ -10,7 +10,7 @@ import {
listenForPrivateUser,
listenForUser,
User,
userDocRef,
users,
} from 'web/lib/firebase/users'
import { useStateCheckEquality } from './use-state-check-equality'
import { identifyUser, setUserProperty } from 'web/lib/service/analytics'
@ -49,7 +49,7 @@ export const usePrivateUser = (userId?: string) => {
export const useUserById = (userId: string) => {
const result = useFirestoreDocumentData<DocumentData, User>(
['users', userId],
userDocRef(userId),
doc(users, userId),
{ subscribe: true, includeMetadataChanges: true }
)

View File

@ -1,6 +1,5 @@
import dayjs from 'dayjs'
import {
getFirestore,
doc,
setDoc,
deleteDoc,
@ -16,8 +15,7 @@ import {
} from 'firebase/firestore'
import { sortBy, sum } from 'lodash'
import { app } from './init'
import { getValues, listenForValue, listenForValues } from './utils'
import { coll, getValues, listenForValue, listenForValues } from './utils'
import { BinaryContract, Contract } from 'common/contract'
import { getDpmProbability } from 'common/calculate-dpm'
import { createRNG, shuffle } from 'common/util/random'
@ -28,6 +26,9 @@ import { MAX_FEED_CONTRACTS } from 'common/recommended-contracts'
import { Bet } from 'common/bet'
import { Comment } from 'common/comment'
import { ENV_CONFIG } from 'common/envs/constants'
export const contracts = coll<Contract>('contracts')
export type { Contract }
export function contractPath(contract: Contract) {
@ -86,83 +87,72 @@ export function tradingAllowed(contract: Contract) {
)
}
const db = getFirestore(app)
export const contractCollection = collection(db, 'contracts')
export const contractDocRef = (contractId: string) =>
doc(db, 'contracts', contractId)
// Push contract to Firestore
export async function setContract(contract: Contract) {
const docRef = doc(db, 'contracts', contract.id)
await setDoc(docRef, contract)
await setDoc(doc(contracts, contract.id), contract)
}
export async function updateContract(
contractId: string,
update: Partial<Contract>
) {
const docRef = doc(db, 'contracts', contractId)
await updateDoc(docRef, update)
await updateDoc(doc(contracts, contractId), update)
}
export async function getContractFromId(contractId: string) {
const docRef = doc(db, 'contracts', contractId)
const result = await getDoc(docRef)
return result.exists() ? (result.data() as Contract) : undefined
const result = await getDoc(doc(contracts, contractId))
return result.exists() ? result.data() : undefined
}
export async function getContractFromSlug(slug: string) {
const q = query(contractCollection, where('slug', '==', slug))
const q = query(contracts, where('slug', '==', slug))
const snapshot = await getDocs(q)
return snapshot.empty ? undefined : (snapshot.docs[0].data() as Contract)
return snapshot.empty ? undefined : snapshot.docs[0].data()
}
export async function deleteContract(contractId: string) {
const docRef = doc(db, 'contracts', contractId)
await deleteDoc(docRef)
await deleteDoc(doc(contracts, contractId))
}
export async function listContracts(creatorId: string): Promise<Contract[]> {
const q = query(
contractCollection,
contracts,
where('creatorId', '==', creatorId),
orderBy('createdTime', 'desc')
)
const snapshot = await getDocs(q)
return snapshot.docs.map((doc) => doc.data() as Contract)
return snapshot.docs.map((doc) => doc.data())
}
export async function listTaggedContractsCaseInsensitive(
tag: string
): Promise<Contract[]> {
const q = query(
contractCollection,
contracts,
where('lowercaseTags', 'array-contains', tag.toLowerCase()),
orderBy('createdTime', 'desc')
)
const snapshot = await getDocs(q)
return snapshot.docs.map((doc) => doc.data() as Contract)
return snapshot.docs.map((doc) => doc.data())
}
export async function listAllContracts(
n: number,
before?: string
): Promise<Contract[]> {
let q = query(contractCollection, orderBy('createdTime', 'desc'), limit(n))
let q = query(contracts, orderBy('createdTime', 'desc'), limit(n))
if (before != null) {
const snap = await getDoc(doc(db, 'contracts', before))
const snap = await getDoc(doc(contracts, before))
q = query(q, startAfter(snap))
}
const snapshot = await getDocs(q)
return snapshot.docs.map((doc) => doc.data() as Contract)
return snapshot.docs.map((doc) => doc.data())
}
export function listenForContracts(
setContracts: (contracts: Contract[]) => void
) {
const q = query(contractCollection, orderBy('createdTime', 'desc'))
const q = query(contracts, orderBy('createdTime', 'desc'))
return listenForValues<Contract>(q, setContracts)
}
@ -171,7 +161,7 @@ export function listenForUserContracts(
setContracts: (contracts: Contract[]) => void
) {
const q = query(
contractCollection,
contracts,
where('creatorId', '==', creatorId),
orderBy('createdTime', 'desc')
)
@ -179,7 +169,7 @@ export function listenForUserContracts(
}
const activeContractsQuery = query(
contractCollection,
contracts,
where('isResolved', '==', false),
where('visibility', '==', 'public'),
where('volume7Days', '>', 0)
@ -196,7 +186,7 @@ export function listenForActiveContracts(
}
const inactiveContractsQuery = query(
contractCollection,
contracts,
where('isResolved', '==', false),
where('closeTime', '>', Date.now()),
where('visibility', '==', 'public'),
@ -214,7 +204,7 @@ export function listenForInactiveContracts(
}
const newContractsQuery = query(
contractCollection,
contracts,
where('isResolved', '==', false),
where('volume7Days', '==', 0),
where('createdTime', '>', Date.now() - 7 * DAY_MS)
@ -230,7 +220,7 @@ export function listenForContract(
contractId: string,
setContract: (contract: Contract | null) => void
) {
const contractRef = doc(contractCollection, contractId)
const contractRef = doc(contracts, contractId)
return listenForValue<Contract>(contractRef, setContract)
}
@ -242,7 +232,7 @@ function chooseRandomSubset(contracts: Contract[], count: number) {
}
const hotContractsQuery = query(
contractCollection,
contracts,
where('isResolved', '==', false),
where('visibility', '==', 'public'),
orderBy('volume24Hours', 'desc'),
@ -262,22 +252,22 @@ export function listenForHotContracts(
}
export async function getHotContracts() {
const contracts = await getValues<Contract>(hotContractsQuery)
const data = await getValues<Contract>(hotContractsQuery)
return sortBy(
chooseRandomSubset(contracts, 10),
chooseRandomSubset(data, 10),
(contract) => -1 * contract.volume24Hours
)
}
export async function getContractsBySlugs(slugs: string[]) {
const q = query(contractCollection, where('slug', 'in', slugs))
const q = query(contracts, where('slug', 'in', slugs))
const snapshot = await getDocs(q)
const contracts = snapshot.docs.map((doc) => doc.data() as Contract)
return sortBy(contracts, (contract) => -1 * contract.volume24Hours)
const data = snapshot.docs.map((doc) => doc.data())
return sortBy(data, (contract) => -1 * contract.volume24Hours)
}
const topWeeklyQuery = query(
contractCollection,
contracts,
where('isResolved', '==', false),
orderBy('volume7Days', 'desc'),
limit(MAX_FEED_CONTRACTS)
@ -287,7 +277,7 @@ export async function getTopWeeklyContracts() {
}
const closingSoonQuery = query(
contractCollection,
contracts,
where('isResolved', '==', false),
where('visibility', '==', 'public'),
where('closeTime', '>', Date.now()),
@ -296,15 +286,12 @@ const closingSoonQuery = query(
)
export async function getClosingSoonContracts() {
const contracts = await getValues<Contract>(closingSoonQuery)
return sortBy(
chooseRandomSubset(contracts, 2),
(contract) => contract.closeTime
)
const data = await getValues<Contract>(closingSoonQuery)
return sortBy(chooseRandomSubset(data, 2), (contract) => contract.closeTime)
}
export async function getRecentBetsAndComments(contract: Contract) {
const contractDoc = doc(db, 'contracts', contract.id)
const contractDoc = doc(contracts, contract.id)
const [recentBets, recentComments] = await Promise.all([
getValues<Bet>(

View File

@ -1,7 +1,7 @@
import {
collection,
deleteDoc,
doc,
getDocs,
query,
updateDoc,
where,
@ -9,11 +9,16 @@ import {
import { sortBy } from 'lodash'
import { Group } from 'common/group'
import { getContractFromId } from './contracts'
import { db } from './init'
import { getValue, getValues, listenForValue, listenForValues } from './utils'
import {
coll,
getValue,
getValues,
listenForValue,
listenForValues,
} from './utils'
import { filterDefined } from 'common/util/array'
const groupCollection = collection(db, 'groups')
export const groups = coll<Group>('groups')
export function groupPath(
groupSlug: string,
@ -23,30 +28,29 @@ export function groupPath(
}
export function updateGroup(group: Group, updates: Partial<Group>) {
return updateDoc(doc(groupCollection, group.id), updates)
return updateDoc(doc(groups, group.id), updates)
}
export function deleteGroup(group: Group) {
return deleteDoc(doc(groupCollection, group.id))
return deleteDoc(doc(groups, group.id))
}
export async function listAllGroups() {
return getValues<Group>(groupCollection)
return getValues<Group>(groups)
}
export function listenForGroups(setGroups: (groups: Group[]) => void) {
return listenForValues(groupCollection, setGroups)
return listenForValues(groups, setGroups)
}
export function getGroup(groupId: string) {
return getValue<Group>(doc(groupCollection, groupId))
return getValue<Group>(doc(groups, groupId))
}
export async function getGroupBySlug(slug: string) {
const q = query(groupCollection, where('slug', '==', slug))
const groups = await getValues<Group>(q)
return groups.length === 0 ? null : groups[0]
const q = query(groups, where('slug', '==', slug))
const docs = (await getDocs(q)).docs
return docs.length === 0 ? null : docs[0].data()
}
export async function getGroupContracts(group: Group) {
@ -68,14 +72,14 @@ export function listenForGroup(
groupId: string,
setGroup: (group: Group | null) => void
) {
return listenForValue(doc(groupCollection, groupId), setGroup)
return listenForValue(doc(groups, groupId), setGroup)
}
export function listenForMemberGroups(
userId: string,
setGroups: (groups: Group[]) => void
) {
const q = query(groupCollection, where('memberIds', 'array-contains', userId))
const q = query(groups, where('memberIds', 'array-contains', userId))
return listenForValues<Group>(q, (groups) => {
const sorted = sortBy(groups, [(group) => -group.mostRecentActivityTime])
@ -87,10 +91,27 @@ export async function getGroupsWithContractId(
contractId: string,
setGroups: (groups: Group[]) => void
) {
const q = query(
groupCollection,
where('contractIds', 'array-contains', contractId)
)
const groups = await getValues<Group>(q)
setGroups(groups)
const q = query(groups, where('contractIds', 'array-contains', contractId))
setGroups(await getValues<Group>(q))
}
export async function joinGroup(group: Group, userId: string): Promise<Group> {
const { memberIds } = group
if (memberIds.includes(userId)) {
return group
}
const newMemberIds = [...memberIds, userId]
const newGroup = { ...group, memberIds: newMemberIds }
await updateGroup(newGroup, { memberIds: newMemberIds })
return newGroup
}
export async function leaveGroup(group: Group, userId: string): Promise<Group> {
const { memberIds } = group
if (!memberIds.includes(userId)) {
return group
}
const newMemberIds = memberIds.filter((id) => id !== userId)
const newGroup = { ...group, memberIds: newMemberIds }
await updateGroup(newGroup, { memberIds: newMemberIds })
return newGroup
}

View File

@ -1,18 +1,12 @@
import {
collection,
getDoc,
orderBy,
query,
setDoc,
where,
} from 'firebase/firestore'
import { getDoc, orderBy, query, setDoc, where } from 'firebase/firestore'
import { doc } from 'firebase/firestore'
import { Manalink } from '../../../common/manalink'
import { db } from './init'
import { customAlphabet } from 'nanoid'
import { listenForValues } from './utils'
import { coll, listenForValues } from './utils'
import { useEffect, useState } from 'react'
export const manalinks = coll<Manalink>('manalinks')
export async function createManalink(data: {
fromId: string
amount: number
@ -45,29 +39,25 @@ export async function createManalink(data: {
message,
}
const ref = doc(db, 'manalinks', slug)
await setDoc(ref, manalink)
await setDoc(doc(manalinks, slug), manalink)
return slug
}
const manalinkCol = collection(db, 'manalinks')
// TODO: This required an index, make sure to also set up in prod
function listUserManalinks(fromId?: string) {
return query(
manalinkCol,
manalinks,
where('fromId', '==', fromId),
orderBy('createdTime', 'desc')
)
}
export async function getManalink(slug: string) {
const docSnap = await getDoc(doc(db, 'manalinks', slug))
return docSnap.data() as Manalink
return (await getDoc(doc(manalinks, slug))).data()
}
export function useManalink(slug: string) {
const [manalink, setManalink] = useState<Manalink | null>(null)
const [manalink, setManalink] = useState<Manalink | undefined>(undefined)
useEffect(() => {
if (slug) {
getManalink(slug).then(setManalink)

View File

@ -1,15 +1,14 @@
import { ManalinkTxn, DonationTxn, TipTxn } from 'common/txn'
import { collection, orderBy, query, where } from 'firebase/firestore'
import { db } from './init'
import { getValues, listenForValues } from './utils'
import { ManalinkTxn, DonationTxn, TipTxn, Txn } from 'common/txn'
import { orderBy, query, where } from 'firebase/firestore'
import { coll, getValues, listenForValues } from './utils'
import { useState, useEffect } from 'react'
import { orderBy as _orderBy } from 'lodash'
const txnCollection = collection(db, 'txns')
export const txns = coll<Txn>('txns')
const getCharityQuery = (charityId: string) =>
query(
txnCollection,
txns,
where('toType', '==', 'CHARITY'),
where('toId', '==', charityId),
orderBy('createdTime', 'desc')
@ -22,7 +21,7 @@ export function listenForCharityTxns(
return listenForValues<DonationTxn>(getCharityQuery(charityId), setTxns)
}
const charitiesQuery = query(txnCollection, where('toType', '==', 'CHARITY'))
const charitiesQuery = query(txns, where('toType', '==', 'CHARITY'))
export function getAllCharityTxns() {
return getValues<DonationTxn>(charitiesQuery)
@ -30,7 +29,7 @@ export function getAllCharityTxns() {
const getTipsQuery = (contractId: string) =>
query(
txnCollection,
txns,
where('category', '==', 'TIP'),
where('data.contractId', '==', contractId)
)
@ -50,13 +49,13 @@ export function useManalinkTxns(userId: string) {
useEffect(() => {
// TODO: Need to instantiate these indexes too
const fromQuery = query(
txnCollection,
txns,
where('fromId', '==', userId),
where('category', '==', 'MANALINK'),
orderBy('createdTime', 'desc')
)
const toQuery = query(
txnCollection,
txns,
where('toId', '==', userId),
where('category', '==', 'MANALINK'),
orderBy('createdTime', 'desc')

View File

@ -1,5 +1,4 @@
import {
getFirestore,
doc,
setDoc,
getDoc,
@ -23,58 +22,62 @@ import {
} from 'firebase/auth'
import { throttle, zip } from 'lodash'
import { app } from './init'
import { app, db } from './init'
import { PortfolioMetrics, PrivateUser, User } from 'common/user'
import { createUser } from './fn-call'
import { getValue, getValues, listenForValue, listenForValues } from './utils'
import {
coll,
getValue,
getValues,
listenForValue,
listenForValues,
} from './utils'
import { feed } from 'common/feed'
import { CATEGORY_LIST } from 'common/categories'
import { safeLocalStorage } from '../util/local'
import { filterDefined } from 'common/util/array'
export const users = coll<User>('users')
export const privateUsers = coll<PrivateUser>('private-users')
export type { User }
export type Period = 'daily' | 'weekly' | 'monthly' | 'allTime'
const db = getFirestore(app)
export const auth = getAuth(app)
export const userDocRef = (userId: string) => doc(db, 'users', userId)
export async function getUser(userId: string) {
const docSnap = await getDoc(userDocRef(userId))
return docSnap.data() as User
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
return (await getDoc(doc(users, userId))).data()!
}
export async function getUserByUsername(username: string) {
// Find a user whose username matches the given username, or null if no such user exists.
const userCollection = collection(db, 'users')
const q = query(userCollection, where('username', '==', username), limit(1))
const docs = await getDocs(q)
const users = docs.docs.map((doc) => doc.data() as User)
return users[0] || null
const q = query(users, where('username', '==', username), limit(1))
const docs = (await getDocs(q)).docs
return docs.length > 0 ? docs[0].data() : null
}
export async function setUser(userId: string, user: User) {
await setDoc(doc(db, 'users', userId), user)
await setDoc(doc(users, userId), user)
}
export async function updateUser(userId: string, update: Partial<User>) {
await updateDoc(doc(db, 'users', userId), { ...update })
await updateDoc(doc(users, userId), { ...update })
}
export async function updatePrivateUser(
userId: string,
update: Partial<PrivateUser>
) {
await updateDoc(doc(db, 'private-users', userId), { ...update })
await updateDoc(doc(privateUsers, userId), { ...update })
}
export function listenForUser(
userId: string,
setUser: (user: User | null) => void
) {
const userRef = doc(db, 'users', userId)
const userRef = doc(users, userId)
return listenForValue<User>(userRef, setUser)
}
@ -82,7 +85,7 @@ export function listenForPrivateUser(
userId: string,
setPrivateUser: (privateUser: PrivateUser | null) => void
) {
const userRef = doc(db, 'private-users', userId)
const userRef = doc(privateUsers, userId)
return listenForValue<PrivateUser>(userRef, setPrivateUser)
}
@ -152,36 +155,29 @@ export async function listUsers(userIds: string[]) {
if (userIds.length > 10) {
throw new Error('Too many users requested at once; Firestore limits to 10')
}
const userCollection = collection(db, 'users')
const q = query(userCollection, where('id', 'in', userIds))
const docs = await getDocs(q)
return docs.docs.map((doc) => doc.data() as User)
const q = query(users, where('id', 'in', userIds))
const docs = (await getDocs(q)).docs
return docs.map((doc) => doc.data())
}
export async function listAllUsers() {
const userCollection = collection(db, 'users')
const q = query(userCollection)
const docs = await getDocs(q)
return docs.docs.map((doc) => doc.data() as User)
const docs = (await getDocs(users)).docs
return docs.map((doc) => doc.data())
}
export function listenForAllUsers(setUsers: (users: User[]) => void) {
const userCollection = collection(db, 'users')
const q = query(userCollection)
listenForValues(q, setUsers)
listenForValues(users, setUsers)
}
export function listenForPrivateUsers(
setUsers: (users: PrivateUser[]) => void
) {
const userCollection = collection(db, 'private-users')
const q = query(userCollection)
listenForValues(q, setUsers)
listenForValues(privateUsers, setUsers)
}
export function getTopTraders(period: Period) {
const topTraders = query(
collection(db, 'users'),
users,
orderBy('profitCached.' + period, 'desc'),
limit(20)
)
@ -191,7 +187,7 @@ export function getTopTraders(period: Period) {
export function getTopCreators(period: Period) {
const topCreators = query(
collection(db, 'users'),
users,
orderBy('creatorVolumeCached.' + period, 'desc'),
limit(20)
)
@ -199,22 +195,21 @@ export function getTopCreators(period: Period) {
}
export async function getTopFollowed() {
const users = await getValues<User>(topFollowedQuery)
return users.slice(0, 20)
return (await getValues<User>(topFollowedQuery)).slice(0, 20)
}
const topFollowedQuery = query(
collection(db, 'users'),
users,
orderBy('followerCountCached', 'desc'),
limit(20)
)
export function getUsers() {
return getValues<User>(collection(db, 'users'))
return getValues<User>(users)
}
export async function getUserFeed(userId: string) {
const feedDoc = doc(db, 'private-users', userId, 'cache', 'feed')
const feedDoc = doc(privateUsers, userId, 'cache', 'feed')
const userFeed = await getValue<{
feed: feed
}>(feedDoc)
@ -222,7 +217,7 @@ export async function getUserFeed(userId: string) {
}
export async function getCategoryFeeds(userId: string) {
const cacheCollection = collection(db, 'private-users', userId, 'cache')
const cacheCollection = collection(privateUsers, userId, 'cache')
const feedData = await Promise.all(
CATEGORY_LIST.map((category) =>
getValue<{ feed: feed }>(doc(cacheCollection, `feed-${category}`))
@ -233,7 +228,7 @@ export async function getCategoryFeeds(userId: string) {
}
export async function follow(userId: string, followedUserId: string) {
const followDoc = doc(db, 'users', userId, 'follows', followedUserId)
const followDoc = doc(collection(users, userId, 'follows'), followedUserId)
await setDoc(followDoc, {
userId: followedUserId,
timestamp: Date.now(),
@ -241,7 +236,7 @@ export async function follow(userId: string, followedUserId: string) {
}
export async function unfollow(userId: string, unfollowedUserId: string) {
const followDoc = doc(db, 'users', userId, 'follows', unfollowedUserId)
const followDoc = doc(collection(users, userId, 'follows'), unfollowedUserId)
await deleteDoc(followDoc)
}
@ -259,7 +254,7 @@ export function listenForFollows(
userId: string,
setFollowIds: (followIds: string[]) => void
) {
const follows = collection(db, 'users', userId, 'follows')
const follows = collection(users, userId, 'follows')
return listenForValues<{ userId: string }>(follows, (docs) =>
setFollowIds(docs.map(({ userId }) => userId))
)

View File

@ -1,10 +1,17 @@
import {
collection,
getDoc,
getDocs,
onSnapshot,
Query,
CollectionReference,
DocumentReference,
} from 'firebase/firestore'
import { db } from './init'
export const coll = <T>(path: string, ...rest: string[]) => {
return collection(db, path, ...rest) as CollectionReference<T>
}
export const getValue = async <T>(doc: DocumentReference) => {
const snap = await getDoc(doc)

View File

@ -4,9 +4,6 @@ const API_DOCS_URL = 'https://docs.manifold.markets/api'
module.exports = {
staticPageGenerationTimeout: 600, // e.g. stats page
reactStrictMode: true,
typescript: {
ignoreBuildErrors: true,
},
experimental: {
externalDir: true,
optimizeCss: true,

View File

@ -14,7 +14,8 @@
"lint": "next lint",
"format": "npx prettier --write .",
"postbuild": "next-sitemap",
"verify": "(cd .. && yarn verify)"
"verify": "(cd .. && yarn verify)",
"verify:dir": "npx prettier --check .; yarn lint --max-warnings 0; tsc --pretty --project tsconfig.json --noEmit"
},
"dependencies": {
"@amplitude/analytics-browser": "0.4.1",

View File

@ -26,6 +26,7 @@ import { useWarnUnsavedChanges } from 'web/hooks/use-warn-unsaved-changes'
import { track } from 'web/lib/service/analytics'
import { GroupSelector } from 'web/components/groups/group-selector'
import { CATEGORIES } from 'common/categories'
import { User } from 'common/user'
export default function Create() {
const [question, setQuestion] = useState('')
@ -33,7 +34,13 @@ export default function Create() {
const router = useRouter()
const { groupId } = router.query as { groupId: string }
useTracking('view create page')
if (!router.isReady) return <div />
const creator = useUser()
useEffect(() => {
if (creator === null) router.push('/')
}, [creator, router])
if (!router.isReady || !creator) return <div />
return (
<Page>
@ -58,7 +65,11 @@ export default function Create() {
</div>
</form>
<Spacer h={6} />
<NewContract question={question} groupId={groupId} />
<NewContract
question={question}
groupId={groupId}
creator={creator}
/>
</div>
</div>
</Page>
@ -66,14 +77,12 @@ export default function Create() {
}
// Allow user to create a new contract
export function NewContract(props: { question: string; groupId?: string }) {
const { question, groupId } = props
const creator = useUser()
useEffect(() => {
if (creator === null) router.push('/')
}, [creator])
export function NewContract(props: {
creator: User
question: string
groupId?: string
}) {
const { creator, question, groupId } = props
const [outcomeType, setOutcomeType] = useState<outcomeType>('BINARY')
const [initialProb] = useState(50)
const [minString, setMinString] = useState('')

View File

@ -15,6 +15,8 @@ import { getUser, User } from 'web/lib/firebase/users'
import { Tabs } from 'web/components/layout/tabs'
import { GroupMembersList } from 'web/pages/group/[...slugs]'
import { checkAgainstQuery } from 'web/hooks/use-sort-and-query-params'
import { SiteLink } from 'web/components/site-link'
import clsx from 'clsx'
export async function getStaticProps() {
const groups = await listAllGroups().catch((_) => [])
@ -105,7 +107,7 @@ export default function Groups(props: {
<Tabs
tabs={[
...(user
...(user && memberGroupIds.length > 0
? [
{
title: 'My Groups',
@ -202,3 +204,16 @@ export function GroupCard(props: { group: Group; creator: User | undefined }) {
</Col>
)
}
export function GroupLink(props: { group: Group; className?: string }) {
const { group, className } = props
return (
<SiteLink
href={groupPath(group.slug)}
className={clsx('z-10 truncate', className)}
>
{group.name}
</SiteLink>
)
}

View File

@ -1,7 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"composite": true,
"baseUrl": "../",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
@ -17,15 +16,9 @@
"jsx": "preserve",
"incremental": true
},
"references": [
{
"path": "../common"
}
],
"watchOptions": {
"excludeDirectories": [".next"]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "../common/**/*.ts"],
"exclude": ["node_modules"]
}