# Project export: Accountable

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2025
- Tagline: Accountable is a social accountability app designed to help users build and maintain positive/healthy habits by leveraging peer accountability and support.
- Devpost: https://devpost.com/software/accountable-tnij3l
- GitHub: https://github.com/akchro/accountability-app
- Video: https://www.youtube.com/embed/HvmFq1HMp6I?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Andy Khau (42 commits), vinngo (23 commits), Brian Loh (7 commits), brianloooooh (1 commits)

## Devpost submission (written by the team)

### Inspiration

Our inspiration for this project came from the app BeReal, which is successful due to its unique approach to social accountability. We decided to take that concept and apply it to the health and wellness space.

### What it does

Accountable at its core is a social accountability app that allows users to create and join focus groups with their friends. Each user is allowed to establish what daily habits they want to build/maintain and mark them as complete for the day. Every user is additionally allowed to view their friend's habits and their progress for the day as long as they belong in the same group. Accountable sends out push notifications reminding users to complete their tasks for the day and also refreshes the progress and timer to complete their habits at 12AM every day. Accountable will also send out push notifications to notify users of group members that didn't complete their task for the day, which is a unique take on social accountability and gives the app an almost competitive feel to it.

### How we built it

We used: Frontend: React Native (Expo) Backend Services: Supabase for authentication, database, and serverless functions Other Service: Twilio (SMS workflow, limited due to free trial constraints) We aimed for a low code backend for Accountable by leveraging Supabase's capabilities to focus more on user experience and group features.

### Challenges we ran into

Learning Curve: For some team members, this was our first time working with modern frameworks like React Native, so some hours were spent brushing up on the basics like HTML and CSS. New Tools: Getting up to speed with React Native and Supabase required hours of reading documentation, trial-and error, and debugging. API Constraints: We had to work around Supabase and Twilio's free trials which meant not as much flexibility in terms of API support. Time Constraints: Shipping MVP meant pivoting and sacrificing some additional features we had in mind for the app.

### Accomplishments we're proud of

Despite all the aforementioned challenges we faced, we pushed through and came out with a working MVP that we hope the CruzHacks judges would love to interact with.

### What we learned

While Accountable is a simple solution in concept, building it came with its fair share of technical and teamwork challenges. We're proud of what we accomplished during CruzHacks and have undoubtedly come out as better developers and team players.

### What's next

We plan to ship features that were not able to make it onto the MVP for Accountable such as image uploading and group leaderboards. This is a project that we are proud of and thus willing to maintain and improve upon, and we hope that Accountable could someday live up to its potential with all of its robust features.

## README (from the GitHub repository)

# Welcome to your Expo app 👋

This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).

## Get started

1. Install dependencies

   ```bash
   npm install
   ```

2. Start the app

   ```bash
    npx expo start
   ```

In the output, you'll find options to open the app in a

- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo

You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).

## Get a fresh project

When you're ready, run:

```bash
npm run reset-project
```

This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.

## Learn more

To learn more about developing your project with Expo, look at the following resources:

- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.

## Join the community

Join our community of developers creating universal apps.

- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.


## Detected evidence (automated analysis)

Indexed codebase: 42 recognized source files, 131 KB.
- JavaScript (language) — detected in the code
- React (technology) — detected in the code
- Supabase (technology) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (47 of 47)

```
.gitignore
app.json
app/_layout.tsx
app/(tabs)/_layout.tsx
app/(tabs)/create-group.tsx
app/(tabs)/dashboard.tsx
app/(tabs)/explore.tsx
app/(tabs)/index.tsx
app/(tabs)/invite-members.tsx
app/(tabs)/join-group.tsx
app/(tabs)/login-screen.tsx
app/(tabs)/profile.tsx
app/+not-found.tsx
app/navigation/AppNavigator.tsx
app/navigation/types.ts
app/styles/dashboard.styles.tsx
app/styles/login-screen.styles.tsx
components/__tests__/__snapshots__/ThemedText-test.tsx.snap
components/__tests__/ThemedText-test.tsx
components/CameraCapture.tsx
components/Collapsible.tsx
components/ExternalLink.tsx
components/HapticTab.tsx
components/HelloWave.tsx
components/ParallaxScrollView.tsx
components/ThemedText.tsx
components/ThemedView.tsx
components/ui/IconSymbol.ios.tsx
components/ui/IconSymbol.tsx
components/ui/TabBarBackground.ios.tsx
components/ui/TabBarBackground.tsx
constants/Colors.ts
hooks/useColorScheme.ts
hooks/useColorScheme.web.ts
hooks/useThemeColor.ts
lib/services/completionService.ts
lib/services/groupService.ts
lib/services/index.ts
lib/services/inviteService.ts
lib/services/taskService.ts
lib/supabase.ts
package.json
README.md
scripts/reset-project.js
tsconfig.json
types/dashboard.ts
types/database.ts
```

### Dependencies

- package.json: @babel/core@^7.25.2, @expo/vector-icons@^14.0.2, @react-native-async-storage/async-storage@1.23.1, @react-navigation/bottom-tabs@^7.2.0, @react-navigation/native@^7.0.14, @rneui/themed@^4.0.0-rc.8, @supabase/supabase-js@^2.49.4, @types/jest@^29.5.12, @types/react@~18.3.12, @types/react-test-renderer@^18.3.0, base64-arraybuffer@^1.0.2, expo@~52.0.43, expo-blur@~14.0.3, expo-camera@~16.0.18, expo-constants@~17.0.8, expo-font@~13.0.4, expo-haptics@~14.0.1, expo-image-manipulator@~13.0.6, expo-linear-gradient@~14.0.2, expo-linking@~7.0.5, expo-media-library@~17.0.6, expo-router@~4.0.20, expo-splash-screen@~0.29.22, expo-status-bar@~2.0.1, expo-symbols@~0.2.2, expo-system-ui@~4.0.9, expo-web-browser@~14.0.2, jest@^29.2.1, jest-expo@~52.0.6, lucide-react-native@^0.487.0, react@18.3.1, react-dom@18.3.1, react-native@0.76.9, react-native-country-codes-picker@^2.3.5, react-native-gesture-handler@~2.20.2, react-native-reanimated@~3.16.1, react-native-safe-area-context@4.12.0, react-native-screens@~4.4.0, react-native-svg@15.8.0, react-native-web@~0.19.13, react-native-webview@13.12.5, react-test-renderer@18.3.1, typescript@^5.3.3

### Recent commits (newest first)

- Merge pull request #29 from vinngo/main
- full photo proof functionality
- Merge pull request #28 from KitchenUtensil/main
- camera
- Merge pull request #27 from vinngo/main
- Merge branch 'main' of https://github.com/vinngo/accountability-app
- Merge branch 'main' of https://github.com/vinngo/accountability-app
- functions to upload image
- Merge pull request #26 from vinngo/main
- Merge branch 'akchro:main' into main
- fixed tsx error
- Merge pull request #25 from vinngo/main
- prep dashboard for image upload capability
- Merge pull request #24 from brianloooooh/main
- final final push
- final profile
- final ui profile
- Merge pull request #23 from KitchenUtensil/main
- fancy profile
- my stuff

## Key source files (fetched from GitHub, selected and truncated for size)

### package.json

```
{
  "name": "accountability-app",
  "main": "expo-router/entry",
  "version": "1.0.0",
  "scripts": {
    "start": "expo start",
    "reset-project": "node ./scripts/reset-project.js",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "test": "jest --watchAll",
    "lint": "expo lint"
  },
  "jest": {
    "preset": "jest-expo"
  },
  "dependencies": {
    "@expo/vector-icons": "^14.0.2",
    "@react-native-async-storage/async-storage": "1.23.1",
    "@react-navigation/bottom-tabs": "^7.2.0",
    "@react-navigation/native": "^7.0.14",
    "@rneui/themed": "^4.0.0-rc.8",
    "@supabase/supabase-js": "^2.49.4",
    "base64-arraybuffer": "^1.0.2",
    "expo": "~52.0.43",
    "expo-blur": "~14.0.3",
    "expo-camera": "~16.0.18",
    "expo-constants": "~17.0.8",
    "expo-font": "~13.0.4",
    "expo-haptics": "~14.0.1",
    "expo-image-manipulator": "~13.0.6",
    "expo-linear-gradient": "~14.0.2",
    "expo-linking": "~7.0.5",
    "expo-media-library": "~17.0.6",
    "expo-router": "~4.0.20",
    "expo-splash-screen": "~0.29.22",
    "expo-status-bar": "~2.0.1",
    "expo-symbols": "~0.2.2",
    "expo-system-ui": "~4.0.9",
    "expo-web-browser": "~14.0.2",
    "lucide-react-native": "^0.487.0",
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "react-native": "0.76.9",
    "react-native-country-codes-picker": "^2.3.5",
    "react-native-gesture-handler": "~2.20.2",
    "react-native-reanimated": "~3.16.1",
    "react-native-safe-area-context": "4.12.0",
    "react-native-screens": "~4.4.0",
    "react-native-svg": "15.8.0",
    "react-native-web": "~0.19.13",
    "react-native-webview": "13.12.5"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/jest": "^29.5.12",
    "@types/react": "~18.3.12",
    "@types/react-test-renderer": "^18.3.0",
    "jest": "^29.2.1",
    "jest-expo": "~52.0.6",
    "react-test-renderer": "18.3.1",
    "typescript": "^5.3.3"
  },
  "private": true
}

```

### app/_layout.tsx

```typescript
import {
  DarkTheme,
  DefaultTheme,
  ThemeProvider,
} from "@react-navigation/native";
import { useFonts } from "expo-font";
import { Stack } from "expo-router";
import * as SplashScreen from "expo-splash-screen";
import { StatusBar } from "expo-status-bar";
import { useEffect } from "react";
import "react-native-reanimated";

import { useColorScheme } from "@/hooks/useColorScheme";
import AppNavigator from "./navigation/AppNavigator";

// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();

export default function RootLayout() {
  const colorScheme = useColorScheme();
  const [loaded] = useFonts({
    SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
  });

  useEffect(() => {
    if (loaded) {
      SplashScreen.hideAsync();
    }
  }, [loaded]);

  if (!loaded) {
    return null;
  }

  return (
    <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
      <Stack>
        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
        <Stack.Screen name="+not-found" />
      </Stack>
      <StatusBar style="auto" />
    </ThemeProvider>
  );
}

```

### lib/services/index.ts

```typescript
export * from './groupService';
export * from './inviteService'; 
```

### app/(tabs)/_layout.tsx

```typescript
import { Stack } from "expo-router";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { useColorScheme } from "@/hooks/useColorScheme";

export default function TabLayout() {
  const colorScheme = useColorScheme();

  return (
    <Stack
      screenOptions={{
        headerShown: false,
        animation: "none",
        gestureEnabled: false,
      }}
    >
      <Stack.Screen name="index"></Stack.Screen>
      <Stack.Screen name="login-screen"></Stack.Screen>
      <Stack.Screen name="create-group"></Stack.Screen>
      <Stack.Screen name="join-group"></Stack.Screen>
      <Stack.Screen name="dashboard"></Stack.Screen>
      <Stack.Screen name="invite-members"></Stack.Screen>
      <Stack.Screen name="profile"></Stack.Screen>
    </Stack>
  );
}

```

### app/(tabs)/index.tsx

```typescript
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator } from "react-native";
import { useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { supabase } from "@/lib/supabase";
import { LinearGradient } from "expo-linear-gradient";

export default function WelcomeScreen() {
  const router = useRouter();
  const [loading, setLoading] = useState(true);
  const [loadingMessage, setLoadingMessage] = useState("Checking authentication...");

  useEffect(() => {
    checkUserSession();
  }, []);

  const checkUserSession = async () => {
    try {
      // Check if user is authenticated
      setLoadingMessage("Checking authentication...");
      const {
        data: { session },
        error,
      } = await supabase.auth.getSession();

      if (error) {
        console.error("Error checking auth:", error);
        setLoading(false);
        return;
      }

      if (!session) {
        // No active session, stay on welcome screen
        setLoading(false);
        return;
      }

      // User is authenticated, check if they exist in profiles table
      setLoadingMessage("Verifying user profile...");
      const { data: profileData, error: profileError } = await supabase
        .from("profiles")
        .select("*")
        .eq("user_id", session.user.id)
        .single();

      if (profileError && profileError.code !== "PGRST116") {
        // An error occurred, but not the "no rows returned" error
        console.error("Error checking profile:", profileError);
        setLoading(false);
        return;
      }

      setLoading(false);

      if (profileData) {
        // User exists in profiles table, redirect to dashboard
        router.push("/dashboard");
      } else {
        // User is authenticated but doesn't have a profile
        console.log("User authenticated but no profile found");
      }
    } catch (error) {
      console.error("Session/profile check failed:", error);
      setLoading(false);
    }
  };

  const handleButtonPress = () => {
    router.push("/login-screen");
  };

  // -----------
  // LOADING STATE
  // -----------
  if (loading) {
    return (
      <LinearGradient colors={["#36D1DC", "#5B86E5"]} style={styles.gradientBackground}>
        <View style={styles.loadingContainer}>
          <ActivityIndicator size="large" color="#fff" />
          <Text style={styles.loadingText}>{loadingMessage}</Text>
        </View>
      </LinearGradient>
    );
  }

  // ---------------
  // WELCOME SCREEN
  // ---------------
  return (
    <LinearGradient colors={["#36D1DC", "#5B86E5"]} style={styles.gradientBackground}>
      <View style={styles.container}>
        <View style={styles.header}>
          <Text style={styles.title}>Accountable</Text>
          <Text style={styles.subtitle}>Creating better habits doesn't have to be done solo</Text>
        </View>

        <TouchableOpacity style={styles.loginButton} onPress={handleButtonPress}>
          <Text style={styles.buttonText}>Login using SMS</Text>
        </TouchableOpacity>
      </View>
    </LinearGradient>
  );
}

// =========================================
// ============     STYLES     ============
// =========================================
const styles = StyleSheet.create({
  gradientBackground: {
    flex: 1,
  },
  container: {
    flex: 1,
    // Center all content vertically
    justifyContent: "center",
    // Center horizontally
    alignItems: "center",
    backgroundColor: "transparent",
    paddingHorizontal: 24,
  },
  // =====================
  // LOADING (Spinner) State
  // =====================
  loadingContainer: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "transparent",
    paddingHorizontal: 24,
  },
  loadingText: {
    marginTop: 16,
    fontSize: 16,
    color: "#fff",
    textAlign: "center",
  },
  // =====================
  // HEADER / TITLE
  // =====================
  header: {
    marginBottom: 40,
    alignItems: "center",
  },
  title: {
    fontSize: 32,
    fontWeight: "700",
    color: "#fff",
    marginBottom: 12,
    textAlign: "center",
  },
  subtitle: {
    fontSize: 16,
    lineHeight: 22,
    color: "#f5f5f5",
    textAlign: "center",
  },
  // =====================
  // BUTTON
  // =====================
  loginButton: {
    backgroundColor: "#5E72E4",
    borderRadius: 10,
    paddingVertical: 16,
    paddingHorizontal: 30,
    // Subtle shadow on iOS; elevation on Android
    shadowColor: "#000",
    shadowOffset: { width: 0, height: 3 },
    shadowOpacity: 0.25,
    shadowRadius: 4,
    elevation: 3,
  },
  buttonText: {
    color: "#fff",
    fontSize: 16,
    fontWeight: "600",
  },
});

```

### hooks/useColorScheme.ts

```typescript
export { useColorScheme } from 'react-native';

```

### types/dashboard.ts

```typescript
/**
 * Interfaces for the Dashboard component
 */

/**
 * Task interface representing a user's task
 */
export interface Task {
  id: string;
  title: string;
  completed: boolean;
  completion?: {
    image_uri: string;
  } | null;
}

/**
 * GroupMember interface representing a member in an accountability group
 */
export interface GroupMember {
  id: string;
  name: string;
  avatar: string;
  tasks: Task[];
  lastCheckin: string;
}

```

### components/ThemedView.tsx

```typescript
import { View, type ViewProps } from 'react-native';

import { useThemeColor } from '@/hooks/useThemeColor';

export type ThemedViewProps = ViewProps & {
  lightColor?: string;
  darkColor?: string;
};

export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
  const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');

  return <View style={[{ backgroundColor }, style]} {...otherProps} />;
}

```

### hooks/useColorScheme.web.ts

```typescript
import { useEffect, useState } from 'react';
import { useColorScheme as useRNColorScheme } from 'react-native';

/**
 * To support static rendering, this value needs to be re-calculated on the client side for web
 */
export function useColorScheme() {
  const [hasHydrated, setHasHydrated] = useState(false);

  useEffect(() => {
    setHasHydrated(true);
  }, []);

  const colorScheme = useRNColorScheme();

  if (hasHydrated) {
    return colorScheme;
  }

  return 'light';
}

```

### hooks/useThemeColor.ts

```typescript
/**
 * Learn more about light and dark modes:
 * https://docs.expo.dev/guides/color-schemes/
 */

import { Colors } from '@/constants/Colors';
import { useColorScheme } from '@/hooks/useColorScheme';

export function useThemeColor(
  props: { light?: string; dark?: string },
  colorName: keyof typeof Colors.light & keyof typeof Colors.dark
) {
  const theme = useColorScheme() ?? 'light';
  const colorFromProps = props[theme];

  if (colorFromProps) {
    return colorFromProps;
  } else {
    return Colors[theme][colorName];
  }
}

```

[32 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]