ExposedGays: map, shop, auth, Supabase schema, Coolify Dockerfile

This commit is contained in:
Ryan Salazar
2026-06-26 17:32:15 -04:00
commit 3eb12c8b34
54 changed files with 4424 additions and 0 deletions

189
src/lib/auth/context.tsx Normal file
View File

@@ -0,0 +1,189 @@
"use client";
import {
createContext,
useContext,
useEffect,
useState,
useCallback,
type ReactNode,
} from "react";
import { createClient } from "@/lib/supabase/client";
import type { User, Session } from "@supabase/supabase-js";
import type { Profile } from "@/types";
interface AuthContextValue {
user: User | null;
session: Session | null;
profile: Profile | null;
loading: boolean;
signUp: (email: string, password: string) => Promise<{ error: string | null }>;
signIn: (email: string, password: string) => Promise<{ error: string | null }>;
signInAnonymously: () => Promise<{ error: string | null }>;
signOut: () => Promise<void>;
updateProfile: (data: Partial<Profile>) => Promise<{ error: string | null }>;
refreshProfile: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
function mapDbProfile(row: Record<string, unknown>): Profile {
const loc = row.location as { coordinates?: [number, number] } | null;
return {
id: row.id as string,
username: (row.username as string) ?? null,
display_name: (row.display_name as string) ?? null,
age: (row.age as number) ?? null,
position: (row.position as Profile["position"]) ?? null,
bio: (row.bio as string) ?? null,
avatar_url: (row.avatar_url as string) ?? null,
status: (row.status as Profile["status"]) ?? "offline",
is_anonymous: (row.is_anonymous as boolean) ?? true,
kinks: (row.kinks as string[]) ?? [],
sti_status: (row.sti_status as string) ?? null,
on_prep: (row.on_prep as boolean) ?? false,
last_active: (row.last_active as string) ?? new Date().toISOString(),
lat: loc?.coordinates?.[1] ?? null,
lng: loc?.coordinates?.[0] ?? null,
city: (row.city as string) ?? null,
vanilla_mode: (row.vanilla_mode as boolean) ?? false,
created_at: (row.created_at as string) ?? new Date().toISOString(),
};
}
export function AuthProvider({ children }: { children: ReactNode }) {
const supabase = createClient();
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(true);
const fetchProfile = useCallback(
async (userId: string) => {
const { data, error } = await supabase
.from("profiles")
.select("*")
.eq("id", userId)
.single();
if (!error && data) {
setProfile(mapDbProfile(data));
}
},
[supabase]
);
useEffect(() => {
supabase.auth.getSession().then(({ data: { session: s } }) => {
setSession(s);
setUser(s?.user ?? null);
if (s?.user) fetchProfile(s.user.id);
setLoading(false);
});
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event, s) => {
setSession(s);
setUser(s?.user ?? null);
if (s?.user) {
fetchProfile(s.user.id);
} else {
setProfile(null);
}
});
return () => subscription.unsubscribe();
}, [supabase, fetchProfile]);
const signUp = async (email: string, password: string) => {
const { error } = await supabase.auth.signUp({
email,
password,
options: { emailRedirectTo: `${window.location.origin}/profile` },
});
return { error: error?.message ?? null };
};
const signIn = async (email: string, password: string) => {
const { error } = await supabase.auth.signInWithPassword({ email, password });
return { error: error?.message ?? null };
};
const signInAnonymously = async () => {
const { error } = await supabase.auth.signInAnonymously();
return { error: error?.message ?? null };
};
const signOut = async () => {
await supabase.auth.signOut();
setProfile(null);
};
const updateProfile = async (data: Partial<Profile>) => {
if (!user) return { error: "Not signed in" };
const payload: Record<string, unknown> = {
updated_at: new Date().toISOString(),
last_active: new Date().toISOString(),
};
if (data.display_name !== undefined) payload.display_name = data.display_name;
if (data.username !== undefined) payload.username = data.username;
if (data.age !== undefined) payload.age = data.age;
if (data.position !== undefined) payload.position = data.position;
if (data.bio !== undefined) payload.bio = data.bio;
if (data.kinks !== undefined) payload.kinks = data.kinks;
if (data.sti_status !== undefined) payload.sti_status = data.sti_status;
if (data.on_prep !== undefined) payload.on_prep = data.on_prep;
if (data.is_anonymous !== undefined) payload.is_anonymous = data.is_anonymous;
if (data.status !== undefined) payload.status = data.status;
if (data.city !== undefined) payload.city = data.city;
const { error } = await supabase
.from("profiles")
.update(payload)
.eq("id", user.id);
if (error) return { error: error.message };
if (data.lat != null && data.lng != null) {
await supabase.rpc("update_profile_location", {
p_user_id: user.id,
p_lat: data.lat,
p_lng: data.lng,
});
}
await fetchProfile(user.id);
return { error: null };
};
const refreshProfile = async () => {
if (user) await fetchProfile(user.id);
};
return (
<AuthContext.Provider
value={{
user,
session,
profile,
loading,
signUp,
signIn,
signInAnonymously,
signOut,
updateProfile,
refreshProfile,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
return ctx;
}