Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add supabase and tailwind utils #45

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions docs/supabase/SQL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Supabase SQL Editor

```sql
-- Enable UUID generation
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- Enum for user roles (user/admin)
DROP TYPE IF EXISTS user_role;
CREATE TYPE user_role AS ENUM ('user', 'admin');

-- Users table with integrated Solana wallet
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username TEXT UNIQUE NOT NULL CHECK (char_length(username) >= 3),
email TEXT UNIQUE NOT NULL,
display_name TEXT NOT NULL,
bio TEXT,
avatar_url TEXT,
solana_wallet_address TEXT UNIQUE NOT NULL, -- Required Solana wallet per user
role user_role NOT NULL DEFAULT 'user',
is_verified BOOLEAN DEFAULT false, -- For verified profiles (like Twitter blue check)
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT username_format CHECK (username ~ '^[a-zA-Z0-9_]+$'),
CONSTRAINT valid_wallet_address CHECK (char_length(solana_wallet_address) > 0)
);

-- Followers table
CREATE TABLE followers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
follower_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
following_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
UNIQUE(follower_id, following_id),
CONSTRAINT no_self_follow CHECK (follower_id != following_id)
);

-- User settings table
CREATE TABLE user_settings (
user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
email_notifications BOOLEAN DEFAULT true,
marketing_emails BOOLEAN DEFAULT true,
theme TEXT DEFAULT 'system', -- system, light, dark
language TEXT DEFAULT 'en',
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- Create indexes
CREATE INDEX idx_users_username ON users(username);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_wallet ON users(solana_wallet_address);
CREATE INDEX idx_followers_follower_id ON followers(follower_id);
CREATE INDEX idx_followers_following_id ON followers(following_id);

-- Add trigger for updated_at
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'UPDATE' THEN
NEW.updated_at = CURRENT_TIMESTAMP;
END IF;
RETURN NEW;
END;
$$ language 'plpgsql';

CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

CREATE TRIGGER update_user_settings_updated_at
BEFORE UPDATE ON user_settings
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

-- Create view for user stats
CREATE OR REPLACE VIEW user_stats AS
SELECT
u.id,
u.username,
u.display_name,
u.avatar_url,
u.is_verified,
COUNT(DISTINCT f1.follower_id) as followers_count,
COUNT(DISTINCT f2.following_id) as following_count
FROM users u
LEFT JOIN followers f1 ON u.id = f1.following_id
LEFT JOIN followers f2 ON u.id = f2.follower_id
GROUP BY u.id, u.username, u.display_name, u.avatar_url, u.is_verified;

-- Disable RLS temporarily for development
ALTER TABLE users DISABLE ROW LEVEL SECURITY;
ALTER TABLE followers DISABLE ROW LEVEL SECURITY;
ALTER TABLE user_settings DISABLE ROW LEVEL SECURITY;

-- Create function to handle new user registration
CREATE OR REPLACE FUNCTION handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
-- Create user settings with defaults
INSERT INTO user_settings (user_id)
VALUES (NEW.id);
RETURN NEW;
END;
$$ language 'plpgsql';

-- Trigger to setup new user
CREATE TRIGGER on_user_created
AFTER INSERT ON users
FOR EACH ROW
EXECUTE FUNCTION handle_new_user();
```
3 changes: 2 additions & 1 deletion web/.env.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
SESSION_SECRET=fill_me
SHYFT_API_KEY=fill_me
WALLET_NETWORK=devnet
WALLET_ADDRESS=fill_me
WALLET_ADDRESS=fill_me
SUPABASE_KEY=fill_me
6 changes: 3 additions & 3 deletions web/app/components/Navigation.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Link, useLocation } from '@remix-run/react'
import clsx from 'clsx'

import { NAVIGATION } from '~/constants'
import { cn } from '~/utils/styles'

export type NavigationProps = {
className?: string
Expand All @@ -11,7 +11,7 @@ export const Navigation: React.FC<NavigationProps> = ({ className }) => {
const location = useLocation()

return (
<nav className={clsx('text-base lg:text-sm', className)}>
<nav className={cn('text-base lg:text-sm', className)}>
<ul className="space-y-9">
{NAVIGATION.map((section) => (
<li key={section.title}>
Expand All @@ -25,7 +25,7 @@ export const Navigation: React.FC<NavigationProps> = ({ className }) => {
<li key={link.href} className="relative">
<Link
to={link.href}
className={clsx(
className={cn(
'block w-full pl-3.5 before:pointer-events-none before:absolute before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full',
link.href === location.pathname
? 'font-semibold text-sky-500 before:bg-sky-500'
Expand Down
4 changes: 2 additions & 2 deletions web/app/components/dialogs/StyledDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Dialog } from '@headlessui/react';
import clsx from 'clsx';
import { AnimatePresence, motion } from 'framer-motion';
import type { PropsWithChildren } from 'react';

import { transitionVariants } from '../../utils/motion';
import { cn } from '~/utils/styles';

interface StyledDialogProps extends PropsWithChildren {
isOpen: boolean;
Expand Down Expand Up @@ -46,7 +46,7 @@ export default function StyledDialog({
initial="growOut"
animate="growIn"
exit="growOut"
className={clsx(
className={cn(
`flex w-full max-w-[92rem] items-center justify-center`,
className
)}
Expand Down
4 changes: 2 additions & 2 deletions web/app/components/dialogs/WalletDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Dialog } from '@headlessui/react'
import { Link } from '@remix-run/react'
import clsx from 'clsx'
import { AnimatePresence, motion } from 'framer-motion'
import { useEffect, useState } from 'react'

import { useSolanaWallet } from '~/hooks'
import { transitionVariants } from '~/utils/motion'
import { cn } from '~/utils/styles'

interface WalletDialogProps {
className?: string
Expand Down Expand Up @@ -53,7 +53,7 @@ export default function WalletDialog({
initial="growOut"
animate="growIn"
exit="growOut"
className={clsx(
className={cn(
`flex w-full max-w-2xl items-center justify-center bg-light p-5 rounded-3xl`,
className
)}
Expand Down
4 changes: 2 additions & 2 deletions web/app/components/ui/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import clsx from 'clsx'
import { cn } from '~/utils/styles'
import type {
HTMLAttributes,
PropsWithChildren,
Expand All @@ -23,7 +23,7 @@ const Avatar = forwardRef<HTMLDivElement, AvatarProps>(
const link = href ?? '#'
return (
<div
className={clsx('flex items-start gap-3 pt-3', className)}
className={cn('flex items-start gap-3 pt-3', className)}
ref={innerRef}
{...rest}
>
Expand Down
4 changes: 2 additions & 2 deletions web/app/components/ui/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import clsx from 'clsx'
import { cn } from '~/utils/styles'
import React, { HTMLAttributes } from 'react'

type BadgeProps = HTMLAttributes<HTMLSpanElement>

const Badge: React.FC<BadgeProps> = ({ children, className, ...rest }) => {
return (
<span
className={clsx(
className={cn(
'mr-2 rounded bg-gray-200 px-2.5 py-2 text-xs font-medium text-gray-800 dark:bg-gray-700 dark:text-gray-300',
className
)}
Expand Down
8 changes: 4 additions & 4 deletions web/app/components/ui/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import clsx from 'clsx'
import React, { ButtonHTMLAttributes } from 'react'
import { cn } from '~/utils/styles'
import type { FC, ButtonHTMLAttributes } from 'react'

type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>

const Button: React.FC<ButtonProps> = ({
const Button: FC<ButtonProps> = ({
children,
className,
type,
...rest
}) => {
return (
<button
className={clsx(
className={cn(
'text-sm text-white md:text-base flex-shrink-0 rounded-md border-4 border-dark-gray bg-dark-gray dark:border-primary dark:bg-primary py-2 px-5 hover:border-gray-800 hover:bg-gray-800 dark:hover:border-primary-tint dark:hover:bg-primary-tint focus:outline-none focus:ring-2 focus:ring-gray-900 focus:ring-offset-2',
className
)}
Expand Down
6 changes: 3 additions & 3 deletions web/app/components/ui/Figure.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import clsx from 'clsx'
import React from 'react'

import { cn } from '~/utils/styles'
import { with3DHover } from '~/hocs'

type FigureProps = React.DetailedHTMLProps<
Expand All @@ -24,15 +24,15 @@ const Figure: React.FC<FigureProps> = ({
}) => {
return (
<figure
className={clsx(
className={cn(
'relative max-w-2xl cursor-pointer grayscale filter transition-all duration-300 hover:grayscale-0',
className
)}
{...rest}
>
<a href={link}>
<img
className={clsx('rounded-2xl bg-indigo-50 object-cover', imgClass)}
className={cn('rounded-2xl bg-indigo-50 object-cover', imgClass)}
src={imgSrc}
alt={imgAlt}
/>
Expand Down
9 changes: 5 additions & 4 deletions web/app/components/ui/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'
import clsx from 'clsx'
import React, { PropsWithChildren } from 'react'
import type { FC, PropsWithChildren } from 'react'

import { cn } from '~/utils/styles'

type PaginationProps = PropsWithChildren & {
className?: string
}

const Pagination: React.FC<PaginationProps> = ({ className }) => {
const Pagination: FC<PaginationProps> = ({ className }) => {
return (
<nav
className={clsx('isolate -space-x-px', className)}
className={cn('isolate -space-x-px', className)}
aria-label="Pagination"
>
<a
Expand Down
10 changes: 4 additions & 6 deletions web/app/components/ui/ThemeButton.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@

import { MoonIcon } from '@heroicons/react/20/solid'
import { SunIcon } from '@heroicons/react/24/outline'
import type { FC, ButtonHTMLAttributes } from 'react'

import clsx from 'clsx'
import React, { ButtonHTMLAttributes } from 'react'

import { cn } from '~/utils/styles'
import { THEME } from '~/constants'
import { useTheme } from '~/theme'

type ThemeButtonProps = ButtonHTMLAttributes<HTMLButtonElement>

const ThemeButton: React.FC<ThemeButtonProps> = ({ className, ...rest }) => {
const ThemeButton: FC<ThemeButtonProps> = ({ className, ...rest }) => {
const [theme, setTheme] = useTheme()
return (
<button
onClick={() =>
setTheme((prev) => (prev === THEME.DARK ? THEME.LIGHT : THEME.DARK))
}
type="button"
className={clsx(
className={cn(
'inline-flex flex-none flex-shrink-0 items-center p-2 text-black',
className
)}
Expand Down
32 changes: 19 additions & 13 deletions web/app/components/ui/header/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/20/solid'
import { Link } from '@remix-run/react'
import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'
import clsx from 'clsx'
import { useWallet } from '@solana/wallet-adapter-react'
import { AnimatePresence, motion } from 'framer-motion'
import React, { useState } from 'react'
import useOnclickOutside from 'react-cool-onclickoutside'
Expand All @@ -12,13 +12,15 @@ import { useScroll } from '~/hooks'
import { MobileNavigation } from '../mobile/Navigation'
import Search from '../search/Search'
import ThemeButton from '../ThemeButton'

import WalletMenu from '~/components/wallet/WalletMenu'
import { cn } from '~/utils/styles'

type HeaderProps = {
title?: string
}

const Header: React.FC<HeaderProps> = ({ title = 'ConcertX' }) => {
const { connected } = useWallet()
const { isScrolled } = useScroll()
const [isSearchFocused, setSearchFocused] = useState(false)
const [isMobileSearchFocused, setMobileSearchFocused] = useState(false)
Expand All @@ -43,7 +45,7 @@ const Header: React.FC<HeaderProps> = ({ title = 'ConcertX' }) => {
<AnimatePresence initial={false}>
<header
ref={headerRef}
className={clsx(
className={cn(
'sticky top-0 z-50 flex flex-wrap items-center justify-between bg-primary-contrast px-4 py-1 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-2xl sm:px-6 lg:px-8',
isScrolled
? 'dark:bg-slate-900/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75'
Expand Down Expand Up @@ -111,16 +113,20 @@ const Header: React.FC<HeaderProps> = ({ title = 'ConcertX' }) => {
)}
<ClientOnly>
{() => (
<WalletMultiButton className="hover:bg-transparent">
{isMobile && (
<img
alt="Wallet"
aria-hidden="true"
src="/assets/wallet_icon.svg"
className="h-7 w-7 dark:invert z-0"
/>
)}
</WalletMultiButton>
connected ? (
<WalletMenu />
) : (
<WalletMultiButton className="hover:bg-transparent">
{isMobile && (
<img
alt="Wallet"
aria-hidden="true"
src="/assets/wallet_icon.svg"
className="h-7 w-7 dark:invert z-0"
/>
)}
</WalletMultiButton>
)
)}
</ClientOnly>
<ThemeButton />
Expand Down
Loading