Project

bcpk: Gear Management for Backpacks

Role:

Everything

Company:

bcpk

Backpackers have a complicated relationship with their gear. Every gram matters when you're carrying everything on your back for days or weeks at a time. Some track weight to the tenth of a gram, while others can tell you exactly how much lighter their pack gets when they eat a Snickers.

Most backpackers manage this intense relationship with just a list or a spreadsheet. More advanced hikers might use LighterPack, an OG tool that's powerful but aging and lacks many modern features.

As a pure passion project, I set out to create a more modern, feature-rich tool that could replace spreadsheets and other existing tools. Using features from products I love like Linear's command menu and list layout, Plex's find match functionality, and LighterPack's 'skip registration' ability, I built a tool that I can happily use for every trip.

Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
Image 1
Image 2
Image 3
Image 4
Image 5
Image 6
Image 7
Image 1
Image 2
Image 3
Image 4

Project Constraints

Building a gear management app might seem straightforward, but backpacking sits at a fascinating intersection of precision data management and real-world utility.

For starters, data visibility is non-negotiable. I want to know what's in my pack, be able to sort on it, see which items take up the most space or weight, and make sure each item is categorized correctly (is it consumable, worn on my body, or in my pack?).

Next, there's the privacy challenge. A growing trend amongst all products is to try to gather as much data from their users as possible. Many people prefer to keep their data private, or simply don't want to enter their email into a product they're unsure of. I doubt many registered LighterPack users even realize that all of their packs are indexed on Google, but many also take their privacy seriously. On the flip side, some people do want to share their setups with the community or with their social network of choice. We needed to accommodate both without compromising either experience.

Then came the platform question. My target users primarily use desktop when planning trips but need mobile access when in the field. For the sake of a manageable scope while developing this app solo, I've focused on the desktop experience until I'm more confident in the feature set.

Screenshots of LighterPack and Linear interfaces

Design inspiration from LighterPack's data model and Linear's command interface

Design-wise, I took inspiration from two main sources: LighterPack's data organization principles (which users already understood) and Linear's command-driven, keyboard-first interface (which enabled powerful workflows without cluttered UI). This combination aimed to create something familiar but significantly improved.

Anonymous flow interface showing local storage usage

The anonymous user flow lets you start immediately with full functionality

Anonymous User Experience

I built the entire pack experience to function without server persistence, using the browser's localStorage API to maintain a complete offline experience. This approach created some interesting technical challenges, particularly around data synchronization if a user later decided to create an account.

// LocalStorageContext implementation
const initializeLocalStorage = useCallback((): LocalStorageUser => {
    const newUser: LocalStorageUser = {
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        id: uuidv4(),
        username: `local_${uuidv4().slice(0, 8)}`,
        email: null,
        name: null,
        role: 'local',
        isEmailVerified: false,
        // Additional user properties with defaults
        weightUnit: 'metric',
        currencyPreference: 'USD'
    };
 
    // Update localStorage
    localStorage.setItem('bcpk_local_user', JSON.stringify(newUser));
    localStorage.setItem('isLocal', 'true');
 
    // Update state
    setIsLocal(true);
    setLocalUser(newUser);
 
    return newUser;
}, []);

The system creates a pseudo-user with a locally-generated ID and default preferences. All packed items, categories, and configurations are then stored relative to this user ID in localStorage. This approach gives anonymous users the full application experience without requiring authentication.

For those concerned about losing data, we built a seamless migration path: when creating an account, we automatically transfer all local data to the server, preserving their existing setups.

Command Menu Navigation

A gear management app needs to handle hundreds of items across multiple packs and categories. Traditional navigation patterns quickly become cumbersome with this many objects to manage and manipulate. Scrolling through a list of 100+ items that have been added and edited over the years is a pain. Using a command menu, users can search for items by name, category, or description, and quickly find and add the item they need.

Command menu interface showing search and available commands

The command menu provides quick access to common actions and navigation

I implemented a command menu system (using the cmdk library) that serves as the central navigation hub for the application. Instead of navigating through multiple menus, users can press Cmd+K (or Ctrl+K) to access nearly any function in the application:

export function GlobalCommandDialog() {
    const { isOpen, closeCommand, currentConfig, handleCommandSelect } = useCommand();
    const { data: packs } = trpc.pack.getPacks.useQuery(undefined, {
        enabled: isOpen
    });
    const getDefaultConfig = useDefaultCommandConfig();
    const [searchQuery, setSearchQuery] = useState('');
 
    const searchResults = useMemo(() => {
        if (!packs || !searchQuery) return undefined;
 
        const filteredPacks = packs.filter(pack =>
            pack.name.toLowerCase().includes(searchQuery.toLowerCase())
        );
 
        if (filteredPacks.length === 0) return undefined;
 
        return {
            heading: `Search results for "${searchQuery}"`,
            items: filteredPacks.map(pack => ({
                id: pack.id,
                title: pack.name,
                type: 'pack',
                url: `/pack/${pack.id}`,
                description: pack.description || 'View pack details',
                keywords: [pack.name, 'pack', 'gear list']
            }))
        };
    }, [packs, searchQuery]);
 
    // Additional functionality...
}

The command menu does more than just navigate—it contextually adapts to the current state of the application. When viewing a pack, commands for adding items, changing categories, or modifying weights appear. When on the dashboard, commands for creating packs or importing data take precedence.

This approach dramatically simplified the interface while making it more powerful. Instead of cluttering the UI with buttons for every possible action, users can simply type what they want to do. Early feedback showed that even less technical users quickly adapted to this pattern and found it significantly faster than traditional navigation.

For power users, we added some keyboard shortcuts throughout the application. Combined with the command menu, this created a highly efficient workflow where experienced users rarely need to reach for their mouse—ideal for planning sessions where you're rapidly cataloging dozens of items.

Customization and Personalization

Backpackers are particular about their gear—and as it turns out, equally particular about their software. We built extensive customization options to let users tailor the experience to their preferences.

Theme customization interface showing light and dark modes

Theme settings with light, dark, and system modes

Avatar generation and customization screen

Avatar generation and upload capabilities

export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
    // Initialize state with values from localStorage
    const [themeMode, setThemeMode] = useState<ThemeMode>(getInitialTheme());
    const [effectiveTheme, setEffectiveTheme] = useState<EffectiveTheme>(
        getEffectiveTheme(getInitialTheme())
    );
 
    // Update effective theme whenever theme mode changes
    useEffect(() => {
        const updateEffectiveTheme = () => {
            setEffectiveTheme(getEffectiveTheme(themeMode));
        };
 
        updateEffectiveTheme();
        localStorage.setItem('theme', themeMode);
 
        // Set up system theme change listener if in system mode
        if (themeMode === 'system') {
            const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
            const handler = () => updateEffectiveTheme();
            mediaQuery.addEventListener('change', handler);
            return () => mediaQuery.removeEventListener('change', handler);
        }
    }, [themeMode]);
 
    // Apply theme to document element
    useEffect(() => {
        document.documentElement.setAttribute('data-theme', effectiveTheme);
    }, [effectiveTheme]);
 
    // Additional implementation...
};

The system uses a combination of localStorage persistence and system preference detection to ensure the theme stays consistent across sessions while respecting user preferences.

For personalization, we implemented avatar generation and customization. Users can either upload their own profile images or generate unique avatars based on their username. This is fairly simple, but it's a fun way to personalize the experience and make sure that user profiles aren't bare.

Account preferences interface showing unit and currency settings

Account preferences with customizable units and currency options

Technical Architecture

Building a modern web application that works across devices while maintaining strict type safety and data integrity required careful architectural planning.

Architecture diagram showing frontend and backend components

PackPkr's technical architecture with React, tRPC, and PostgreSQL

The frontend is built with React and TypeScript, leveraging Radix UI for accessible, composable components. This combination provided excellent developer experience while ensuring the UI remained accessible and performant.

For data communication, we used tRPC to create type-safe API endpoints. This approach eliminated the common disconnects between frontend and backend typing, as the TypeScript interfaces are shared across the entire application:

// Server router definition
export const packRouter = createTRPCRouter({
    getPacks: protectedProcedure
        .query(async ({ ctx }) => {
            const packs = await ctx.db.pack.findMany({
                where: { userId: ctx.user.id },
                orderBy: { updatedAt: 'desc' }
            });
            return packs;
        }),
    // Additional endpoints...
});
 
// Client consumption is fully typed
const { data: packs } = trpc.pack.getPacks.useQuery();

The backend uses Node.js with PostgreSQL for data persistence. We implemented a connection pooling strategy to optimize database performance, along with repository and service patterns to keep the business logic cleanly separated:

// Repository pattern implementation
export class PackRepository {
    constructor(private db: Db) {}
 
    async findById(id: string): Promise<Pack | null> {
        return this.db.pack.findUnique({
            where: { id },
            include: {
                lists: {
                    include: {
                        items: true
                    }
                }
            }
        });
    }
 
    // Additional methods...
};

For deployment, I use Render.com with automatic database migrations through a custom script. This setup allows for simplified database management, which as a designer by trade, I'm fairly new to.

Testing is implemented with Jest and React Testing Library, with specific attention to data consistency and state management. The database access layer is thoroughly tested to ensure data integrity across all operations. Again, as a designer, having tests that ensure my data is always consistent is extremely valuable while I move quickly to build new features.

User testimonials and feedback

Initial user feedback highlighted the speed and precision of PackPkr

Outcomes and Lessons

Several key lessons emerged from this project:

Quality over quantity. Overall, I'd much rather have fewer features that work perfectly than more features that don't get used. This is harder than it sounds when you have a long list of ideas that sound like great additions.

Privacy isn't just about security. The anonymous mode wasn't originally planned as a core feature, but looking at recent competitors' launches it was something that was commented on by many users.

Command patterns reduce cognitive load. The command menu approach dramatically simplified the interface while making it more powerful. It's still a new pattern, so users need to be habituated to it, but it's a powerful way to make complex workflows more accessible.

TypeScript pays dividends. The investment in strict typing across the entire application prevented countless bugs and made feature development significantly faster as the project grew.

Building bcpk stretched my capabilities as both a designer and developer. Balancing the precision needs of ultralight backpackers with the simplicity required by casual users forced constant reconsideration of interface patterns and data models.

The application continues to evolve based on user feedback, with plans to add trip planning features, weather integration, and expanded sharing capabilities. What began as a focused tool for gear management is growing into a comprehensive platform for the backpacking community—all while maintaining the precision, privacy, and power that defined the original vision.

Just for fun

Animation
+