diff --git a/next.config.js b/next.config.js index 829fd3c..04360ac 100644 --- a/next.config.js +++ b/next.config.js @@ -5,6 +5,15 @@ import './src/env.js'; /** @type {import("next").NextConfig} */ -const config = {}; +const config = { + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: '*.gbrown.org', + }, + ], + }, +}; export default config; diff --git a/src/actions/image.ts b/src/actions/image.ts new file mode 100644 index 0000000..0857bb3 --- /dev/null +++ b/src/actions/image.ts @@ -0,0 +1,4 @@ +'use server'; + +import 'server-only'; +import { createClient } from '@/utils/supabase/server'; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 72acbaf..f488abb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -4,6 +4,7 @@ import { Geist } from 'next/font/google'; import { cn } from '@/lib/utils'; import { ThemeProvider } from '@/components/context/theme'; import Navigation from '@/components/navigation'; +import Footer from '@/components/footer'; export const metadata: Metadata = { title: 'T3 Template with Supabase', @@ -46,10 +47,11 @@ const RootLayout = ({ children }: Readonly<{ children: React.ReactNode }>) => {
-
+
{children}
+
diff --git a/src/app/page.tsx b/src/app/page.tsx index 4509f3b..b62cabc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,13 +1,9 @@ const HomePage = () => { return ( -
-
+
+
+

Make sure you can sign in!

+
); }; diff --git a/src/app/test/page.tsx b/src/app/test/page.tsx new file mode 100644 index 0000000..8eebf78 --- /dev/null +++ b/src/app/test/page.tsx @@ -0,0 +1,60 @@ +'use server'; + +import { createClient } from '@/utils/supabase/server'; +import Image from 'next/image'; + +export default async function Page() { + const supabase = await createClient(); + + // Get authenticated user + const { + data: { user: authUser }, + error: userError, + } = await supabase.auth.getUser(); + + if (userError || !authUser) { + return ( +
+ Error loading user: {userError?.message ?? 'User not authenticated'} +
+ ); + } + + // Get user profile + const { data: user, error: profileError } = await supabase + .from('profiles') + .select('*') + .eq('id', authUser.id) + .single(); + + if (profileError || !user) { + return ( +
+ Error loading profile: {profileError?.message ?? 'Profile not found'} +
+ ); + } + + // Check if avatar URL exists + if (!user.avatar_url) { + return
No avatar image available
; + } + + // Get public URL for the avatar + const { data: imageData } = await supabase.storage + .from('avatars') + .createSignedUrl(user.avatar_url, 3600); + + return ( +
+ User avatar +
+ ); +} diff --git a/src/components/context/theme.tsx b/src/components/context/theme.tsx index eecc9dd..3d9aea5 100644 --- a/src/components/context/theme.tsx +++ b/src/components/context/theme.tsx @@ -20,7 +20,12 @@ export const ThemeProvider = ({ return {children}; }; -export const ThemeToggle = () => { +export interface ThemeToggleProps + extends React.ButtonHTMLAttributes { + size?: number; +} + +export const ThemeToggle = ({ size = 1, ...props }: ThemeToggleProps) => { const { setTheme, resolvedTheme } = useTheme(); const [mounted, setMounted] = React.useState(false); @@ -30,8 +35,8 @@ export const ThemeToggle = () => { if (!mounted) { return ( - ); } @@ -42,14 +47,14 @@ export const ThemeToggle = () => { }; return ( - diff --git a/src/components/footer/index.tsx b/src/components/footer/index.tsx new file mode 100644 index 0000000..84a02ef --- /dev/null +++ b/src/components/footer/index.tsx @@ -0,0 +1,20 @@ +'use server'; + +const FooterTest = () => { + return ( + + ); +}; +export default FooterTest; diff --git a/src/components/navigation/auth.tsx b/src/components/navigation/auth.tsx index 5a52b45..549d51e 100644 --- a/src/components/navigation/auth.tsx +++ b/src/components/navigation/auth.tsx @@ -22,7 +22,7 @@ const NavigationAuth = async () => { ) : (
-
- +
+ + +
); diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 657477e..00bab66 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -26,6 +26,7 @@ const buttonVariants = cva( sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', icon: 'size-9', + smicon: 'size-6', }, }, defaultVariants: { diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/server/db/schema.sql b/src/server/db/schema.sql new file mode 100644 index 0000000..9ade50e --- /dev/null +++ b/src/server/db/schema.sql @@ -0,0 +1,105 @@ +-- Create a table for public profiles +create table profiles ( + id uuid references auth.users on delete cascade not null primary key, + updated_at timestamp with time zone, + email text, + full_name text, + avatar_url text, + provider text, + + constraint full_name_length check (char_length(full_name) >= 3 and char_length(full_name) <= 50) +); +-- Set up Row Level Security (RLS) +-- See https://supabase.com/docs/guides/auth/row-level-security for more details. +alter table profiles + enable row level security; + +create policy "Public profiles are viewable by everyone." on profiles + for select using (true); + +create policy "Users can insert their own profile." on profiles + for insert with check ((select auth.uid()) = id); + +create policy "Users can update own profile." on profiles + for update using ((select auth.uid()) = id); + +-- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth. +-- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details. +create function public.handle_new_user() +returns trigger +set search_path = '' +as $$ +begin + insert into public.profiles (id, email, full_name, avatar_url, provider, updated_at) + values ( + new.id, + new.email, + new.raw_user_meta_data->>'full_name', + new.raw_user_meta_data->>'avatar_url' + new.raw_user_meta_data->>'provider', + now() + ); + return new; +end; +$$ language plpgsql security definer; +create trigger on_auth_user_created + after insert on auth.users + for each row execute procedure public.handle_new_user(); + +-- Set up Storage! +insert into storage.buckets (id, name) + values ('avatars', 'avatars'); + +-- Set up access controls for storage. +-- See https://supabase.com/docs/guides/storage#policy-examples for more details. +create policy "Avatar images are publicly accessible." on storage.objects + for select using (bucket_id = 'avatars'); + +create policy "Anyone can upload an avatar." on storage.objects + for insert with check (bucket_id = 'avatars'); + + +-- -- Create a table for public statuses +-- CREATE TABLE statuses ( + -- id uuid DEFAULT gen_random_uuid() PRIMARY KEY, + -- user_id uuid REFERENCES auth.users ON DELETE CASCADE NOT NULL, + -- updated_by_id uuid REFERENCES auth.users ON DELETE SET NULL DEFAULT auth.uid(), + -- created_at timestamp with time zone DEFAULT now() NOT NULL, + -- status text NOT NULL, + -- CONSTRAINT status_length CHECK (char_length(status) >= 3 AND char_length(status) <= 80), + -- CONSTRAINT statuses_user_id_fkey FOREIGN KEY (user_id) REFERENCES profiles(id) ON DELETE CASCADE +-- ); + +-- -- Set up Row Level Security (RLS) +-- ALTER TABLE statuses + -- ENABLE ROW LEVEL SECURITY; + +-- -- Policies +-- CREATE POLICY "Public statuses are viewable by everyone." ON statuses + -- FOR SELECT USING (true); + +-- CREATE POLICY "Users can insert statuses for any user." ON statuses + -- FOR INSERT WITH CHECK (auth.role() = 'authenticated'); + +-- -- Function to add first status +-- CREATE FUNCTION public.handle_first_status() +-- RETURNS TRIGGER +-- SET search_path = '' +-- AS $$ +-- BEGIN + -- INSERT INTO public.statuses (user_id, updated_by_id, status) + -- VALUES ( + -- NEW.id, + -- NEW.id, + -- 'Just joined!' + -- ); + -- RETURN NEW; +-- END; +-- $$ LANGUAGE plpgsql SECURITY DEFINER; + +-- -- Create a separate trigger for the status +-- CREATE TRIGGER on_auth_user_created_add_status + -- AFTER INSERT ON auth.users + -- FOR EACH ROW EXECUTE PROCEDURE public.handle_first_status(); + +-- alter publication supabase_realtime add table statuses;