From 683056dc2ae91005e4f15357fba72f1c4eae74b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Nov 2025 08:07:46 +0000 Subject: [PATCH] Build fullshtack - plain-language website builder Create a guided website builder that teaches while you build: - Landing page with anti-magic pitch explaining the philosophy - 5-step wizard flow: project basics, data modeling (nouns), authentication, pages/routing, and styling - Component library showing functional legos with dependencies - Dashboard view summarizing the entire project setup - Plain language throughout - no jargon, just explanations All explanations translate tech concepts to normal words. The whole point: understand what you're building before you build it. --- pages/1_what_are_we_building.py | 151 ++++++++ pages/2_what_are_the_nouns.py | 241 ++++++++++++ pages/3_how_do_people_get_in.py | 208 ++++++++++ pages/4_what_pages_exist.py | 259 +++++++++++++ pages/5_pick_a_vibe.py | 249 ++++++++++++ pages/6_component_library.py | 653 ++++++++++++++++++++++++++++++++ pages/7_your_schtack.py | 290 ++++++++++++++ requirements.txt | 4 +- streamlit_app.py | 186 +++++++-- utils/__init__.py | 1 + utils/content.py | 292 ++++++++++++++ utils/state.py | 158 ++++++++ 12 files changed, 2662 insertions(+), 30 deletions(-) create mode 100644 pages/1_what_are_we_building.py create mode 100644 pages/2_what_are_the_nouns.py create mode 100644 pages/3_how_do_people_get_in.py create mode 100644 pages/4_what_pages_exist.py create mode 100644 pages/5_pick_a_vibe.py create mode 100644 pages/6_component_library.py create mode 100644 pages/7_your_schtack.py create mode 100644 utils/__init__.py create mode 100644 utils/content.py create mode 100644 utils/state.py diff --git a/pages/1_what_are_we_building.py b/pages/1_what_are_we_building.py new file mode 100644 index 000000000000..1495ecd13813 --- /dev/null +++ b/pages/1_what_are_we_building.py @@ -0,0 +1,151 @@ +""" +Step 1: What are we building? +The plain-language project description. +""" +import streamlit as st +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from utils.state import init_state, get_project, update_project, mark_step_complete +from utils.content import STEP_EXPLANATIONS, ERROR_MESSAGES, SUCCESS_MESSAGES + +st.set_page_config( + page_title="what are we building? | fullshtack", + page_icon="", + layout="wide" +) + +# Initialize +init_state() +project = get_project() +step_content = STEP_EXPLANATIONS["project"] + +# Header +st.markdown(f"## {step_content['title']}") +st.markdown(f"*{step_content['subtitle']}*") + +# Two column layout - form on left, explanation on right +col1, col2 = st.columns([3, 2]) + +with col1: + st.markdown(step_content["explanation"]) + + st.markdown("---") + + # Project name + project_name = st.text_input( + "what should we call this thing?", + value=project.project_name, + placeholder="my awesome project, the thing, literally anything", + help="working title. you can change it later. probably will." + ) + + # Project description + project_description = st.text_area( + "what does it do? (explain it like you're telling a friend)", + value=project.project_description, + placeholder="It's a tool that helps people track their... / It's a site where users can browse and... / It connects people who have X with people who need Y...", + height=150, + help="don't overthink it. we just need to understand what we're building." + ) + + # Project type + st.markdown("what kind of thing is this, roughly?") + project_type = st.radio( + "project type", + options=["app", "site", "tool", "marketplace", "dashboard", "other"], + horizontal=True, + index=["app", "site", "tool", "marketplace", "dashboard", "other"].index(project.project_type) if project.project_type else 0, + label_visibility="collapsed", + help="this helps us suggest sensible defaults later" + ) + + st.markdown("---") + + # Framework choice with plain explanation + st.markdown("### quick tech decision") + st.markdown("what should we build this with? (don't stress, they're all good)") + + framework = st.selectbox( + "framework", + options=["nextjs", "sveltekit", "astro"], + index=["nextjs", "sveltekit", "astro"].index(project.framework) if project.framework else 0, + format_func=lambda x: { + "nextjs": "Next.js - React-based, most popular, tons of resources", + "sveltekit": "SvelteKit - simpler syntax, less boilerplate, gaining momentum", + "astro": "Astro - great for content sites, ships less JavaScript" + }[x], + label_visibility="collapsed" + ) + + database = st.selectbox( + "database", + options=["supabase", "planetscale", "sqlite"], + index=["supabase", "planetscale", "sqlite"].index(project.database) if project.database else 0, + format_func=lambda x: { + "supabase": "Supabase - Postgres with auth built in, generous free tier", + "planetscale": "PlanetScale - MySQL, scales well, branching like git", + "sqlite": "SQLite - simple file-based, good for smaller projects" + }[x], + label_visibility="collapsed" + ) + + st.markdown("---") + + # Validation and save + col_back, col_spacer, col_next = st.columns([1, 2, 1]) + + with col_back: + if st.button(" back to start", use_container_width=True): + st.switch_page("streamlit_app.py") + + with col_next: + if st.button("next: the nouns ", type="primary", use_container_width=True): + # Validate + if not project_name.strip(): + st.error(ERROR_MESSAGES["empty_name"]) + elif not project_description.strip(): + st.error(ERROR_MESSAGES["empty_description"]) + else: + # Save + update_project( + project_name=project_name.strip(), + project_description=project_description.strip(), + project_type=project_type, + framework=framework, + database=database + ) + mark_step_complete(0) + st.success(SUCCESS_MESSAGES["project_saved"]) + st.switch_page("pages/2_what_are_the_nouns.py") + +with col2: + st.markdown("### why this matters") + st.markdown(step_content["why_this_matters"]) + + st.markdown("---") + + st.markdown("### the tech translation") + st.markdown(""" + What we're doing here in nerd speak: + + - **Project name** → your app's identifier + - **Description** → the README, basically + - **Type** → informs architecture decisions + - **Framework** → the thing that structures your code + - **Database** → where your data lives + + But you don't need to think about it that way. + We're just figuring out what we're building. + """) + + if project.project_name: + st.markdown("---") + st.markdown("### current setup") + st.code(f""" +project: {project.project_name} +type: {project.project_type or 'not set'} +stack: {project.framework} + {project.database} + """, language="yaml") diff --git a/pages/2_what_are_the_nouns.py b/pages/2_what_are_the_nouns.py new file mode 100644 index 000000000000..c32741cce394 --- /dev/null +++ b/pages/2_what_are_the_nouns.py @@ -0,0 +1,241 @@ +""" +Step 2: What are the nouns? +Data modeling without saying "schema". +""" +import streamlit as st +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from utils.state import init_state, get_project, update_project, mark_step_complete, Noun +from utils.content import STEP_EXPLANATIONS, ERROR_MESSAGES, SUCCESS_MESSAGES + +st.set_page_config( + page_title="what are the nouns? | fullshtack", + page_icon="", + layout="wide" +) + +# Initialize +init_state() +project = get_project() +step_content = STEP_EXPLANATIONS["nouns"] + +# Header +st.markdown(f"## {step_content['title']}") +st.markdown(f"*{step_content['subtitle']}*") + +# Initialize session state for nouns editing +if 'editing_nouns' not in st.session_state: + st.session_state.editing_nouns = list(project.nouns) if project.nouns else [] +if 'show_add_noun' not in st.session_state: + st.session_state.show_add_noun = False + +col1, col2 = st.columns([3, 2]) + +with col1: + st.markdown(step_content["explanation"]) + + st.markdown("---") + + # Common nouns suggestions based on project type + st.markdown("### common nouns (click to add)") + + common_nouns = { + "app": ["Users", "Posts", "Comments", "Likes", "Messages", "Notifications"], + "site": ["Users", "Pages", "Posts", "Categories", "Tags", "Media"], + "tool": ["Users", "Projects", "Tasks", "Settings", "Logs"], + "marketplace": ["Users", "Sellers", "Buyers", "Listings", "Orders", "Reviews", "Messages"], + "dashboard": ["Users", "Reports", "Metrics", "Settings", "Exports"], + "other": ["Users", "Items", "Categories", "Settings"] + } + + suggestions = common_nouns.get(project.project_type, common_nouns["other"]) + existing_names = [n.name.lower() for n in st.session_state.editing_nouns] + + cols = st.columns(4) + for i, suggestion in enumerate(suggestions): + if suggestion.lower() not in existing_names: + with cols[i % 4]: + if st.button(f"+ {suggestion}", key=f"add_{suggestion}"): + new_noun = Noun( + name=suggestion, + description=f"The {suggestion.lower()} in your app", + fields=[], + relationships=[] + ) + st.session_state.editing_nouns.append(new_noun) + st.rerun() + + st.markdown("---") + + # Current nouns + st.markdown("### your nouns") + + if not st.session_state.editing_nouns: + st.info("no nouns yet. add some above or create a custom one below.") + else: + for i, noun in enumerate(st.session_state.editing_nouns): + with st.expander(f"**{noun.name}**", expanded=True): + # Basic info + new_name = st.text_input( + "name", + value=noun.name, + key=f"noun_name_{i}", + label_visibility="collapsed" + ) + + new_desc = st.text_input( + "what is this?", + value=noun.description, + key=f"noun_desc_{i}", + placeholder="a brief description of what this thing is" + ) + + # Fields + st.markdown("**what info do you track about each one?**") + st.caption("(these become columns in your database)") + + # Common field suggestions + common_fields = { + "users": ["email", "name", "password", "avatar", "created_at"], + "posts": ["title", "content", "author", "published_at", "status"], + "products": ["name", "description", "price", "image", "stock"], + "orders": ["user", "items", "total", "status", "created_at"], + "messages": ["from", "to", "content", "read", "sent_at"], + } + + field_suggestions = common_fields.get(noun.name.lower(), ["name", "description", "created_at"]) + + # Show current fields + if noun.fields: + for j, field in enumerate(noun.fields): + fcol1, fcol2, fcol3 = st.columns([2, 2, 1]) + with fcol1: + st.text(field.get("name", "")) + with fcol2: + st.text(field.get("type", "text")) + with fcol3: + if st.button("x", key=f"remove_field_{i}_{j}"): + noun.fields.pop(j) + st.rerun() + + # Add field + fcol1, fcol2, fcol3 = st.columns([2, 2, 1]) + with fcol1: + new_field_name = st.text_input( + "field name", + key=f"new_field_name_{i}", + placeholder="field name", + label_visibility="collapsed" + ) + with fcol2: + new_field_type = st.selectbox( + "type", + options=["text", "number", "email", "password", "date", "boolean", "reference"], + key=f"new_field_type_{i}", + label_visibility="collapsed" + ) + with fcol3: + if st.button("add", key=f"add_field_{i}"): + if new_field_name: + noun.fields.append({"name": new_field_name, "type": new_field_type}) + st.rerun() + + # Quick add common fields + st.caption("quick add:") + qcols = st.columns(len(field_suggestions)) + for k, fs in enumerate(field_suggestions): + existing_field_names = [f["name"] for f in noun.fields] + if fs not in existing_field_names: + with qcols[k]: + if st.button(fs, key=f"quick_{i}_{fs}"): + field_type = "email" if "email" in fs else "password" if "password" in fs else "date" if "_at" in fs else "text" + noun.fields.append({"name": fs, "type": field_type}) + st.rerun() + + st.markdown("---") + + # Update the noun + st.session_state.editing_nouns[i].name = new_name + st.session_state.editing_nouns[i].description = new_desc + + # Remove button + if st.button(f"remove {noun.name}", key=f"remove_noun_{i}", type="secondary"): + st.session_state.editing_nouns.pop(i) + st.rerun() + + st.markdown("---") + + # Add custom noun + st.markdown("### add a custom noun") + + with st.expander("create new noun"): + custom_name = st.text_input("what's the thing called?", key="custom_noun_name") + custom_desc = st.text_input("what is it?", key="custom_noun_desc", placeholder="a brief description") + + if st.button("add this noun"): + if custom_name: + existing_names = [n.name.lower() for n in st.session_state.editing_nouns] + if custom_name.lower() in existing_names: + st.error(ERROR_MESSAGES["duplicate_noun"]) + else: + new_noun = Noun( + name=custom_name, + description=custom_desc or f"The {custom_name.lower()} in your app", + fields=[], + relationships=[] + ) + st.session_state.editing_nouns.append(new_noun) + st.rerun() + + st.markdown("---") + + # Navigation + col_back, col_spacer, col_next = st.columns([1, 2, 1]) + + with col_back: + if st.button(" back", use_container_width=True): + st.switch_page("pages/1_what_are_we_building.py") + + with col_next: + if st.button("next: authentication ", type="primary", use_container_width=True): + if not st.session_state.editing_nouns: + st.error(ERROR_MESSAGES["no_nouns"]) + else: + update_project(nouns=st.session_state.editing_nouns) + mark_step_complete(1) + st.success(SUCCESS_MESSAGES["nouns_saved"]) + st.switch_page("pages/3_how_do_people_get_in.py") + +with col2: + st.markdown("### why this matters") + st.markdown(step_content["why_this_matters"]) + + st.markdown("---") + + st.markdown("### the tech translation") + st.markdown(""" + What we're doing here in nerd speak: + + - **Nouns** → database tables / models / entities + - **Fields** → columns in those tables + - **Types** → data types (string, integer, boolean, etc.) + - **Relationships** → foreign keys, joins + + The fancy term is "data modeling" or "schema design". + + But really: we're just listing the things and what we know about them. + """) + + if st.session_state.editing_nouns: + st.markdown("---") + st.markdown("### your data model") + for noun in st.session_state.editing_nouns: + st.markdown(f"**{noun.name}**") + if noun.fields: + for field in noun.fields: + st.caption(f" - {field['name']} ({field['type']})") + else: + st.caption(" - (no fields yet)") diff --git a/pages/3_how_do_people_get_in.py b/pages/3_how_do_people_get_in.py new file mode 100644 index 000000000000..db0bf7d3be54 --- /dev/null +++ b/pages/3_how_do_people_get_in.py @@ -0,0 +1,208 @@ +""" +Step 3: How do people get in? +Authentication without the jargon. +""" +import streamlit as st +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from utils.state import init_state, get_project, update_project, mark_step_complete +from utils.content import STEP_EXPLANATIONS, SUCCESS_MESSAGES + +st.set_page_config( + page_title="how do people get in? | fullshtack", + page_icon="", + layout="wide" +) + +# Initialize +init_state() +project = get_project() +step_content = STEP_EXPLANATIONS["auth"] + +# Header +st.markdown(f"## {step_content['title']}") +st.markdown(f"*{step_content['subtitle']}*") + +col1, col2 = st.columns([3, 2]) + +with col1: + st.markdown(step_content["explanation"]) + + st.markdown("---") + + # Auth method selection + st.markdown("### pick your login method") + + auth_options = { + "email": { + "label": "email & password", + "description": "classic. they sign up with email, pick a password, log in with those.", + "icon": "" + }, + "google": { + "label": "google / social login", + "description": "one click with their Google account. less friction, but you're dependent on Google.", + "icon": "" + }, + "both": { + "label": "both options", + "description": "let them choose. more work to set up, but maximum flexibility.", + "icon": "" + }, + "none": { + "label": "no login needed", + "description": "public tool, no user accounts. everyone sees the same thing.", + "icon": "" + } + } + + # Current selection + current_auth = project.auth_method or "email" + + for method, info in auth_options.items(): + is_selected = current_auth == method + container = st.container() + with container: + col_radio, col_text = st.columns([1, 10]) + with col_radio: + if st.checkbox( + info["label"], + value=is_selected, + key=f"auth_{method}", + label_visibility="collapsed" + ): + current_auth = method + with col_text: + st.markdown(f"**{info['icon']} {info['label']}**") + st.caption(info["description"]) + + st.markdown("---") + + # Additional features (only if auth is needed) + if current_auth != "none": + st.markdown("### extra login features") + st.caption("check what you want. we'll set it up.") + + features = { + "password_reset": "forgot password / reset by email", + "remember_me": "stay logged in (remember me checkbox)", + "email_verify": "verify email before account works", + "magic_link": "passwordless login via email link", + "two_factor": "two-factor authentication (SMS or app)" + } + + selected_features = project.auth_features or [] + + for feature_key, feature_label in features.items(): + is_checked = feature_key in selected_features + if st.checkbox(feature_label, value=is_checked, key=f"feature_{feature_key}"): + if feature_key not in selected_features: + selected_features.append(feature_key) + else: + if feature_key in selected_features: + selected_features.remove(feature_key) + + st.markdown("---") + + # Visual representation of what this means + st.markdown("### what this looks like in practice") + + preview_col1, preview_col2 = st.columns(2) + + with preview_col1: + st.markdown("**sign up page will have:**") + if current_auth in ["email", "both"]: + st.markdown("- email field") + st.markdown("- password field") + st.markdown("- confirm password field") + if current_auth in ["google", "both"]: + st.markdown("- 'continue with Google' button") + if "email_verify" in selected_features: + st.markdown("- sends verification email after signup") + + with preview_col2: + st.markdown("**login page will have:**") + if current_auth in ["email", "both"]: + st.markdown("- email field") + st.markdown("- password field") + if current_auth in ["google", "both"]: + st.markdown("- 'continue with Google' button") + if "remember_me" in selected_features: + st.markdown("- 'remember me' checkbox") + if "password_reset" in selected_features: + st.markdown("- 'forgot password?' link") + if "magic_link" in selected_features: + st.markdown("- 'email me a login link' option") + + else: + selected_features = [] + st.info("no login means everyone can access everything. make sure that's what you want.") + + st.markdown("---") + + # Navigation + col_back, col_spacer, col_next = st.columns([1, 2, 1]) + + with col_back: + if st.button(" back", use_container_width=True): + st.switch_page("pages/2_what_are_the_nouns.py") + + with col_next: + if st.button("next: pages ", type="primary", use_container_width=True): + update_project( + auth_method=current_auth, + auth_features=selected_features + ) + mark_step_complete(2) + st.success(SUCCESS_MESSAGES["auth_saved"]) + st.switch_page("pages/4_what_pages_exist.py") + +with col2: + st.markdown("### why this matters") + st.markdown(step_content["why_this_matters"]) + + st.markdown("---") + + st.markdown("### the tech translation") + st.markdown(""" + What we're doing here in nerd speak: + + - **Authentication** → proving who someone is + - **Email/password** → credential-based auth + - **Google login** → OAuth 2.0 flow + - **Remember me** → persistent session/token + - **Password reset** → token-based password recovery + - **Email verify** → email confirmation flow + - **Magic link** → passwordless auth via email + - **2FA** → multi-factor authentication + + Your database setup (**{database}**) handles most of this for you. + We're just deciding what options to turn on. + """.format(database=project.database)) + + st.markdown("---") + + st.markdown("### security note") + st.markdown(""" + We're not rolling our own auth. That's how security holes happen. + + Your stack ({database}) has battle-tested auth built in. + We're using that. You just pick the features. + + - Passwords get hashed (unreadable even to us) + - Sessions are secure + - Tokens expire properly + - All the boring-but-critical stuff is handled + """.format(database=project.database)) + + if current_auth != "none": + st.markdown("---") + st.markdown("### your auth setup") + st.code(f""" +method: {current_auth} +features: +{chr(10).join(f' - {f}' for f in selected_features) if selected_features else ' (none selected)'} + """, language="yaml") diff --git a/pages/4_what_pages_exist.py b/pages/4_what_pages_exist.py new file mode 100644 index 000000000000..6f94681d04f8 --- /dev/null +++ b/pages/4_what_pages_exist.py @@ -0,0 +1,259 @@ +""" +Step 4: What pages exist? +Routing without the routing jargon. +""" +import streamlit as st +import sys +from pathlib import Path +import re + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from utils.state import init_state, get_project, update_project, mark_step_complete, Page +from utils.content import STEP_EXPLANATIONS, ERROR_MESSAGES, SUCCESS_MESSAGES + +st.set_page_config( + page_title="what pages exist? | fullshtack", + page_icon="", + layout="wide" +) + +# Initialize +init_state() +project = get_project() +step_content = STEP_EXPLANATIONS["pages"] + +# Header +st.markdown(f"## {step_content['title']}") +st.markdown(f"*{step_content['subtitle']}*") + +# Initialize session state for pages +if 'editing_pages' not in st.session_state: + st.session_state.editing_pages = list(project.pages) if project.pages else [] + +col1, col2 = st.columns([3, 2]) + +with col1: + st.markdown(step_content["explanation"]) + + st.markdown("---") + + # Common page suggestions based on project type and auth + st.markdown("### common pages (click to add)") + + base_pages = ["Home", "About"] + + auth_pages = [] + if project.auth_method and project.auth_method != "none": + auth_pages = ["Sign Up", "Log In", "Profile", "Settings"] + + # Pages based on nouns + noun_pages = [] + for noun in project.nouns: + noun_pages.append(f"{noun.name} List") + noun_pages.append(f"{noun.name} Detail") + if project.auth_method != "none": + noun_pages.append(f"Create {noun.name}") + + all_suggestions = base_pages + auth_pages + noun_pages + existing_names = [p.name.lower() for p in st.session_state.editing_pages] + + cols = st.columns(3) + for i, suggestion in enumerate(all_suggestions[:12]): # Limit to 12 + if suggestion.lower() not in existing_names: + with cols[i % 3]: + if st.button(f"+ {suggestion}", key=f"add_page_{suggestion}"): + # Generate path from name + path = "/" + re.sub(r'[^a-z0-9]+', '-', suggestion.lower()).strip('-') + if path == "/home": + path = "/" + + # Determine if auth required + requires_auth = suggestion in ["Profile", "Settings"] or "Create" in suggestion + + # Connect to nouns + connected = [] + for noun in project.nouns: + if noun.name.lower() in suggestion.lower(): + connected.append(noun.name) + + new_page = Page( + name=suggestion, + path=path, + description=f"The {suggestion.lower()} page", + requires_auth=requires_auth, + connected_nouns=connected + ) + st.session_state.editing_pages.append(new_page) + st.rerun() + + st.markdown("---") + + # Current pages + st.markdown("### your pages") + + if not st.session_state.editing_pages: + st.info("no pages yet. add some above or create a custom one below.") + else: + for i, page in enumerate(st.session_state.editing_pages): + with st.expander(f"**{page.name}** `{page.path}`", expanded=False): + # Page name + new_name = st.text_input( + "page name", + value=page.name, + key=f"page_name_{i}" + ) + + # Path + new_path = st.text_input( + "url path", + value=page.path, + key=f"page_path_{i}", + help="like /about or /users/123" + ) + + # Description + new_desc = st.text_input( + "what's on this page?", + value=page.description, + key=f"page_desc_{i}" + ) + + # Requires auth? + if project.auth_method != "none": + new_requires_auth = st.checkbox( + "requires login to see?", + value=page.requires_auth, + key=f"page_auth_{i}" + ) + else: + new_requires_auth = False + + # Connected nouns + if project.nouns: + st.markdown("**which nouns show up here?**") + new_connected = [] + noun_cols = st.columns(len(project.nouns)) + for j, noun in enumerate(project.nouns): + with noun_cols[j]: + if st.checkbox( + noun.name, + value=noun.name in page.connected_nouns, + key=f"page_noun_{i}_{j}" + ): + new_connected.append(noun.name) + else: + new_connected = [] + + # Update + st.session_state.editing_pages[i].name = new_name + st.session_state.editing_pages[i].path = new_path + st.session_state.editing_pages[i].description = new_desc + st.session_state.editing_pages[i].requires_auth = new_requires_auth + st.session_state.editing_pages[i].connected_nouns = new_connected + + st.markdown("---") + + if st.button(f"remove this page", key=f"remove_page_{i}", type="secondary"): + st.session_state.editing_pages.pop(i) + st.rerun() + + st.markdown("---") + + # Add custom page + st.markdown("### add a custom page") + + with st.expander("create new page"): + custom_name = st.text_input("page name", key="custom_page_name") + custom_path = st.text_input( + "url path", + key="custom_page_path", + placeholder="/something", + help="lowercase, no spaces, start with /" + ) + custom_desc = st.text_input("what's on this page?", key="custom_page_desc") + + custom_auth = False + if project.auth_method != "none": + custom_auth = st.checkbox("requires login?", key="custom_page_auth") + + custom_nouns = [] + if project.nouns: + st.markdown("**uses which nouns?**") + for noun in project.nouns: + if st.checkbox(noun.name, key=f"custom_noun_{noun.name}"): + custom_nouns.append(noun.name) + + if st.button("add this page"): + if custom_name and custom_path: + # Validate path + if not custom_path.startswith("/") or " " in custom_path: + st.error(ERROR_MESSAGES["invalid_path"]) + else: + new_page = Page( + name=custom_name, + path=custom_path, + description=custom_desc or f"The {custom_name.lower()} page", + requires_auth=custom_auth, + connected_nouns=custom_nouns + ) + st.session_state.editing_pages.append(new_page) + st.rerun() + + st.markdown("---") + + # Navigation + col_back, col_spacer, col_next = st.columns([1, 2, 1]) + + with col_back: + if st.button(" back", use_container_width=True): + st.switch_page("pages/3_how_do_people_get_in.py") + + with col_next: + if st.button("next: styling ", type="primary", use_container_width=True): + if not st.session_state.editing_pages: + st.error(ERROR_MESSAGES["no_pages"]) + else: + update_project(pages=st.session_state.editing_pages) + mark_step_complete(3) + st.success(SUCCESS_MESSAGES["pages_saved"]) + st.switch_page("pages/5_pick_a_vibe.py") + +with col2: + st.markdown("### why this matters") + st.markdown(step_content["why_this_matters"]) + + st.markdown("---") + + st.markdown("### the tech translation") + st.markdown(""" + What we're doing here in nerd speak: + + - **Pages** → routes in your framework + - **Path** → the URL pattern (`/users`, `/posts/[id]`) + - **Requires login** → protected routes / auth guards + - **Connected nouns** → which data this page fetches + + Your framework ({framework}) handles the actual routing. + We're just mapping out what exists. + """.format(framework=project.framework)) + + if st.session_state.editing_pages: + st.markdown("---") + st.markdown("### your sitemap") + + for page in st.session_state.editing_pages: + auth_badge = " (login)" if page.requires_auth else "" + nouns_badge = f" [{', '.join(page.connected_nouns)}]" if page.connected_nouns else "" + st.markdown(f"`{page.path}` - {page.name}{auth_badge}{nouns_badge}") + + # Visual tree + st.markdown("---") + st.markdown("### route tree") + routes = [p.path for p in st.session_state.editing_pages] + routes.sort() + for route in routes: + depth = route.count("/") - 1 if route != "/" else 0 + indent = " " * depth + name = route.split("/")[-1] or "home" + st.code(f"{indent}{name}", language=None) diff --git a/pages/5_pick_a_vibe.py b/pages/5_pick_a_vibe.py new file mode 100644 index 000000000000..94dd418e0f13 --- /dev/null +++ b/pages/5_pick_a_vibe.py @@ -0,0 +1,249 @@ +""" +Step 5: Pick a vibe. +Styling without the CSS trauma. +""" +import streamlit as st +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from utils.state import init_state, get_project, update_project, mark_step_complete +from utils.content import STEP_EXPLANATIONS, SUCCESS_MESSAGES + +st.set_page_config( + page_title="pick a vibe | fullshtack", + page_icon="", + layout="wide" +) + +# Initialize +init_state() +project = get_project() +step_content = STEP_EXPLANATIONS["styling"] + +# Header +st.markdown(f"## {step_content['title']}") +st.markdown(f"*{step_content['subtitle']}*") + +# Style definitions with visual examples +VIBES = { + "clean": { + "name": "clean & corporate", + "description": "lots of white space, professional, trustworthy. think: stripe, notion.", + "colors": { + "primary": "#0066FF", + "secondary": "#6B7280", + "background": "#FFFFFF", + "surface": "#F9FAFB", + "text": "#111827", + "border": "#E5E7EB" + }, + "border_radius": "8px", + "font": "Inter, system-ui, sans-serif", + "shadows": True + }, + "friendly": { + "name": "friendly & rounded", + "description": "softer edges, warmer colors, approachable. think: slack, figma.", + "colors": { + "primary": "#7C3AED", + "secondary": "#EC4899", + "background": "#FFFBEB", + "surface": "#FEF3C7", + "text": "#1F2937", + "border": "#FCD34D" + }, + "border_radius": "16px", + "font": "Nunito, system-ui, sans-serif", + "shadows": True + }, + "minimal": { + "name": "minimal & sharp", + "description": "less is more, stark contrasts, modern. think: linear, vercel.", + "colors": { + "primary": "#000000", + "secondary": "#666666", + "background": "#FFFFFF", + "surface": "#FAFAFA", + "text": "#000000", + "border": "#EEEEEE" + }, + "border_radius": "4px", + "font": "SF Mono, monospace", + "shadows": False + }, + "brutalist": { + "name": "brutalist & weird", + "description": "intentionally raw, anti-design. think: are.na, cargo.", + "colors": { + "primary": "#FF0000", + "secondary": "#0000FF", + "background": "#FFFFFF", + "surface": "#FFFF00", + "text": "#000000", + "border": "#000000" + }, + "border_radius": "0px", + "font": "Times New Roman, serif", + "shadows": False + } +} + +col1, col2 = st.columns([3, 2]) + +with col1: + st.markdown(step_content["explanation"]) + + st.markdown("---") + + # Vibe selection with live preview + st.markdown("### pick your vibe") + + current_vibe = project.style_vibe or "clean" + + for vibe_key, vibe_data in VIBES.items(): + is_selected = current_vibe == vibe_key + + # Create a visual card for each vibe + colors = vibe_data["colors"] + border = f"3px solid {colors['primary']}" if is_selected else f"1px solid {colors['border']}" + + st.markdown(f""" +
+
+
+

{vibe_data['name']}

+

+ {vibe_data['description']} +

+
+
+
+ + +
+
+

+ this is what a card would look like. notice the corners, the spacing, the overall feel. +

+
+
+ """, unsafe_allow_html=True) + + if st.button( + f"{'selected' if is_selected else 'select'} {vibe_data['name']}", + key=f"select_{vibe_key}", + type="primary" if is_selected else "secondary", + disabled=is_selected + ): + current_vibe = vibe_key + st.rerun() + + st.markdown("---") + + # Color customization + st.markdown("### customize the primary color") + st.caption("or just keep the default. it's fine.") + + selected_vibe_data = VIBES[current_vibe] + default_color = selected_vibe_data["colors"]["primary"] + + primary_color = st.color_picker( + "primary color", + value=project.primary_color if project.primary_color != "#0068c9" else default_color, + label_visibility="collapsed" + ) + + st.markdown("---") + + # Navigation + col_back, col_spacer, col_next = st.columns([1, 2, 1]) + + with col_back: + if st.button(" back", use_container_width=True): + st.switch_page("pages/4_what_pages_exist.py") + + with col_next: + if st.button("finish setup ", type="primary", use_container_width=True): + update_project( + style_vibe=current_vibe, + primary_color=primary_color + ) + mark_step_complete(4) + st.success(SUCCESS_MESSAGES["all_done"]) + st.balloons() + st.switch_page("pages/7_your_schtack.py") + +with col2: + st.markdown("### why this matters") + st.markdown(step_content["why_this_matters"]) + + st.markdown("---") + + st.markdown("### the tech translation") + st.markdown(""" + What we're doing here in nerd speak: + + - **Vibe** → design system / theme + - **Colors** → design tokens / CSS variables + - **Border radius** → corner rounding + - **Font** → typography stack + - **Shadows** → elevation / depth + + We'll generate CSS variables from this: + + ```css + :root { + --color-primary: #...; + --color-secondary: #...; + --radius: 8px; + --font-body: Inter, sans-serif; + } + ``` + + Then every component uses these variables. + Change the vibe, everything updates. + """) + + st.markdown("---") + + st.markdown("### your style config") + vibe_data = VIBES[current_vibe] + st.code(f""" +vibe: {current_vibe} +primary: {primary_color} +secondary: {vibe_data['colors']['secondary']} +border-radius: {vibe_data['border_radius']} +font: {vibe_data['font'].split(',')[0]} +shadows: {vibe_data['shadows']} + """, language="yaml") diff --git a/pages/6_component_library.py b/pages/6_component_library.py new file mode 100644 index 000000000000..b8bb088e202e --- /dev/null +++ b/pages/6_component_library.py @@ -0,0 +1,653 @@ +""" +Component Library - Functional Legos +Each block shows what it is, what it needs, and how it connects. +""" +import streamlit as st +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from utils.state import init_state, get_project +from utils.content import COMPONENT_EXPLANATIONS + +st.set_page_config( + page_title="component library | fullshtack", + page_icon="", + layout="wide" +) + +# Initialize +init_state() +project = get_project() + +# Custom CSS for component previews +st.markdown(""" + +""", unsafe_allow_html=True) + +# Header +st.markdown("## component library") +st.markdown("*functional legos - each one shows what it is, what it needs, what it connects to*") + +st.markdown(""" +These aren't just pretty pictures of UI elements. + +Each component here is a **contract**: +- What data does it expect? +- What does it do with that data? +- What happens when things go wrong? +- How does it talk to other components? + +Click any component to see the full picture. +""") + +st.markdown("---") + +# Component categories +COMPONENTS = { + "inputs": { + "name": "getting input from people", + "items": [ + { + "name": "Text Input", + "description": "a box where someone types something", + "props": ["label", "placeholder", "value", "onChange", "validation"], + "dependencies": [], + "connects_to": ["Forms", "State"], + "preview_html": """ +
+ + +
+ """, + "code_example": """ +// What this looks like in your code + setEmail(e.target.value)} + validation={{ + required: true, + pattern: /^[^@]+@[^@]+$/, + errorMessage: "that doesn't look like an email" + }} +/> + """, + "gotchas": "Always validate on the server too. The browser can be tricked." + }, + { + "name": "Password Input", + "description": "like text input but hidden, with optional show/hide toggle", + "props": ["label", "value", "onChange", "showToggle", "minLength"], + "dependencies": [], + "connects_to": ["Forms", "Auth"], + "preview_html": """ +
+ + +
+ """, + "code_example": """ + setPassword(e.target.value)} + showToggle={true} + minLength={8} + requirements={["uppercase", "number"]} +/> + """, + "gotchas": "Never store passwords in plain text. Your auth system handles this." + }, + { + "name": "Select / Dropdown", + "description": "pick one thing from a list of things", + "props": ["label", "options", "value", "onChange", "placeholder"], + "dependencies": [], + "connects_to": ["Forms", "State", "API (for dynamic options)"], + "preview_html": """ +
+ + +
+ """, + "code_example": """ + + I agree to the terms + + + """, + "code_example": """ + setAgreedToTerms(e.target.checked)} +/> + """, + "gotchas": "For 'required' checkboxes (like terms), validate before form submit." + } + ] + }, + "actions": { + "name": "things people click", + "items": [ + { + "name": "Button", + "description": "a clickable thing that does something when clicked", + "props": ["label", "onClick", "type", "disabled", "loading"], + "dependencies": [], + "connects_to": ["Forms", "API calls", "Navigation"], + "preview_html": """ +
+ + +
+ """, + "code_example": """ + + +// The onClick should handle: +// 1. Sending data to your API +// 2. Showing loading state +// 3. Handling success (show message, redirect) +// 4. Handling errors (show what went wrong) + """, + "gotchas": "Buttons inside forms submit the form. Add type='button' if you don't want that." + }, + { + "name": "Link Button", + "description": "looks like a button, but goes somewhere instead of doing something", + "props": ["href", "label", "variant"], + "dependencies": ["Router"], + "connects_to": ["Pages", "Navigation"], + "preview_html": """ +
+ Go to dashboard +
+ """, + "code_example": """ + + Go to dashboard + + +// For external links, add target="_blank" and rel="noopener" + """, + "gotchas": "For internal navigation, use your framework's Link component for faster page loads." + } + ] + }, + "layout": { + "name": "organizing stuff on the page", + "items": [ + { + "name": "Card", + "description": "a contained box that groups related content together", + "props": ["children", "title", "footer", "onClick"], + "dependencies": [], + "connects_to": ["Lists", "Grids", "Data display"], + "preview_html": """ +
+
image
+
+

Product Name

+

A brief description of this item that might wrap to two lines.

+

$99.00

+
+
+ """, + "code_example": """ + goToProduct(product.id)}> + + + {product.name} + {product.description} + {product.price} + + + """, + "gotchas": "Keep cards in a grid consistent - same info in same positions." + }, + { + "name": "Navigation", + "description": "the menu that lets people move between pages", + "props": ["items", "currentPath", "user"], + "dependencies": ["Router", "Auth state"], + "connects_to": ["All pages", "Auth"], + "preview_html": """ + + """, + "code_example": """ + + """, + "gotchas": "Should show different options for logged-in vs logged-out users. Should indicate current page." + }, + { + "name": "Modal", + "description": "a popup that appears over the page, demanding attention", + "props": ["isOpen", "onClose", "title", "children"], + "dependencies": [], + "connects_to": ["Any trigger button", "Forms"], + "preview_html": """ +
+
+
+

Delete item?

+ × +
+

This action cannot be undone. The item will be permanently removed.

+
+ + +
+
+
+ """, + "code_example": """ + setShowDeleteModal(false)}> + Delete item? + + This action cannot be undone. The item will be permanently removed. + + + + + + + """, + "gotchas": "Don't overuse. Trap focus inside for accessibility. Close on escape key." + } + ] + }, + "data": { + "name": "showing data from your database", + "items": [ + { + "name": "Data Table", + "description": "data organized in rows and columns, like a spreadsheet", + "props": ["columns", "data", "onRowClick", "sortable", "pagination"], + "dependencies": ["API / Data fetching"], + "connects_to": ["Database", "Detail pages"], + "preview_html": """ +
+ + + + + + + + + + + + +
NameEmailStatus
Alice Johnsonalice@example.comActive
Bob Smithbob@example.comPending
+
+ """, + "code_example": """ + } + ]} + data={users} // from your API + onRowClick={(user) => router.push(`/users/${user.id}`)} + pagination={{ page, pageSize: 10, total }} +/> + """, + "gotchas": "Tables are bad on mobile. Consider card layouts for small screens. Don't load 10,000 rows." + }, + { + "name": "List", + "description": "items displayed one after another, vertically", + "props": ["items", "renderItem", "onItemClick", "emptyState"], + "dependencies": ["API / Data fetching"], + "connects_to": ["Database", "Detail pages"], + "preview_html": """ +
+
+
+
+
New message from Alice
+
Hey, are you coming to the meeting?
+
+
+
+
+
+
Payment received
+
Order #1234 has been paid
+
+
+
+ """, + "code_example": """ +No notifications yet

} + renderItem={(item) => ( + handleNotification(item)}> + + + {item.title} + {item.message} + + + )} +/> + """, + "gotchas": "Handle empty state. Handle loading state. For long lists, use virtualization." + }, + { + "name": "Empty State", + "description": "what to show when there's nothing to show", + "props": ["title", "description", "action"], + "dependencies": [], + "connects_to": ["Lists", "Tables", "Any data display"], + "preview_html": """ +
+
*
+

No products yet

+

Create your first product to get started

+ +
+ """, + "code_example": """ +{products.length === 0 ? ( + setShowCreateModal(true)}>Add product} + /> +) : ( + +)} + """, + "gotchas": "Every list should have an empty state. It's a teaching moment." + } + ] + }, + "forms": { + "name": "collecting and submitting data", + "items": [ + { + "name": "Form", + "description": "a container for inputs that collects data and sends it somewhere", + "props": ["onSubmit", "children", "validation"], + "dependencies": ["API endpoint"], + "connects_to": ["API", "Database", "State"], + "preview_html": """ +
+
+ + +
+
+ + +
+ +
+ """, + "code_example": """ +
+ + + + Log in + + + + +// The onSubmit function should: +// 1. Validate all fields +// 2. Send to API +// 3. Handle success +// 4. Handle and display errors + """, + "gotchas": "Always validate server-side too. Show clear error messages. Disable submit while loading." + } + ] + }, + "feedback": { + "name": "telling people what happened", + "items": [ + { + "name": "Alert / Toast", + "description": "a temporary message that tells people something happened", + "props": ["type", "message", "duration", "onClose"], + "dependencies": [], + "connects_to": ["Any action that needs feedback"], + "preview_html": """ +
+
+ + Changes saved successfully +
+
+ ! Something went wrong. Please try again. +
+
+ """, + "code_example": """ +// After a successful action: +toast.success("Changes saved successfully") + +// After an error: +toast.error("Something went wrong. Please try again.") + +// With more control: +Upgrade now} +/> + """, + "gotchas": "Don't stack too many. Auto-dismiss success, but let errors stick. Be specific about what happened." + }, + { + "name": "Loading State", + "description": "showing that something is happening, please wait", + "props": ["loading", "children"], + "dependencies": [], + "connects_to": ["Any async operation"], + "preview_html": """ +
+
+

Loading your data...

+
+ """, + "code_example": """ +{isLoading ? ( + +) : ( + +)} + +// Or with a wrapper: + + + + """, + "gotchas": "Always show loading states. Skeleton loaders are better than spinners for layouts." + } + ] + } +} + +# Sidebar - category filter +with st.sidebar: + st.markdown("### categories") + selected_category = st.radio( + "filter", + options=["all"] + list(COMPONENTS.keys()), + format_func=lambda x: COMPONENTS[x]["name"] if x != "all" else "all components", + label_visibility="collapsed" + ) + + st.markdown("---") + + st.markdown("### your nouns") + if project.nouns: + for noun in project.nouns: + st.markdown(f"- {noun.name}") + st.caption("components can be wired to these") + else: + st.caption("no nouns defined yet") + + st.markdown("---") + + if st.button(" back to setup"): + st.switch_page("pages/1_what_are_we_building.py") + +# Main content +categories_to_show = [selected_category] if selected_category != "all" else COMPONENTS.keys() + +for cat_key in categories_to_show: + cat_data = COMPONENTS[cat_key] + st.markdown(f"### {cat_data['name']}") + + for component in cat_data["items"]: + with st.expander(f"**{component['name']}** - {component['description']}"): + # Preview + st.markdown("#### what it looks like") + st.markdown(component["preview_html"], unsafe_allow_html=True) + + col1, col2 = st.columns(2) + + with col1: + st.markdown("#### what it needs (props)") + for prop in component["props"]: + st.markdown(f'{prop}', unsafe_allow_html=True) + + st.markdown("") + st.markdown("#### connects to") + for conn in component["connects_to"]: + st.markdown(f'{conn}', unsafe_allow_html=True) + + with col2: + st.markdown("#### dependencies") + if component["dependencies"]: + for dep in component["dependencies"]: + st.markdown(f"- {dep}") + else: + st.caption("none - standalone component") + + st.markdown("#### watch out for") + st.warning(component["gotchas"]) + + st.markdown("#### example code") + st.code(component["code_example"], language="jsx") + + # Connect to project nouns + if project.nouns: + st.markdown("#### wire to your data") + st.markdown("this component could display or interact with:") + cols = st.columns(len(project.nouns)) + for i, noun in enumerate(project.nouns): + with cols[i]: + st.checkbox(noun.name, key=f"{cat_key}_{component['name']}_{noun.name}") + + st.markdown("---") diff --git a/pages/7_your_schtack.py b/pages/7_your_schtack.py new file mode 100644 index 000000000000..1dfd8e20502c --- /dev/null +++ b/pages/7_your_schtack.py @@ -0,0 +1,290 @@ +""" +Your Schtack - The Dashboard +Everything you've set up, in one place. Ready to generate. +""" +import streamlit as st +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from utils.state import init_state, get_project, get_progress_summary, export_project_config, reset_project +from utils.content import SUCCESS_MESSAGES + +st.set_page_config( + page_title="your schtack | fullshtack", + page_icon="", + layout="wide" +) + +# Initialize +init_state() +project = get_project() +progress = get_progress_summary() + +# Header +st.markdown(f"## your schtack: {project.project_name or 'unnamed project'}") +st.markdown("*everything you've set up, in one place*") + +if progress["is_complete"]: + st.success(SUCCESS_MESSAGES["all_done"]) +else: + st.warning("your schtack isn't complete yet. finish the setup to generate code.") + +st.markdown("---") + +# Overview columns +col1, col2, col3 = st.columns(3) + +with col1: + st.markdown("### the basics") + st.markdown(f"**project:** {project.project_name or 'not set'}") + st.markdown(f"**type:** {project.project_type or 'not set'}") + st.markdown(f"**framework:** {project.framework}") + st.markdown(f"**database:** {project.database}") + + if project.project_description: + st.markdown("---") + st.markdown("**what it does:**") + st.markdown(f"*{project.project_description}*") + +with col2: + st.markdown("### your data model") + if project.nouns: + for noun in project.nouns: + with st.expander(f"**{noun.name}**"): + if noun.description: + st.caption(noun.description) + if noun.fields: + for field in noun.fields: + st.markdown(f"- `{field['name']}` ({field['type']})") + else: + st.caption("no fields defined") + else: + st.caption("no nouns defined yet") + +with col3: + st.markdown("### authentication") + if project.auth_method: + st.markdown(f"**method:** {project.auth_method}") + if project.auth_features: + st.markdown("**features:**") + for feature in project.auth_features: + feature_labels = { + "password_reset": "password reset", + "remember_me": "remember me", + "email_verify": "email verification", + "magic_link": "magic link login", + "two_factor": "two-factor auth" + } + st.markdown(f"- {feature_labels.get(feature, feature)}") + else: + st.caption("not configured yet") + +st.markdown("---") + +# Pages section +st.markdown("### your pages") + +if project.pages: + page_cols = st.columns(min(4, len(project.pages))) + for i, page in enumerate(project.pages): + with page_cols[i % 4]: + auth_icon = "" if page.requires_auth else "" + st.markdown(f""" +
+
{auth_icon} {page.name}
+
{page.path}
+
{page.description or 'no description'}
+
+ """, unsafe_allow_html=True) + if page.connected_nouns: + st.caption(f"uses: {', '.join(page.connected_nouns)}") +else: + st.caption("no pages defined yet") + +st.markdown("---") + +# Styling section +st.markdown("### the vibe") + +if project.style_vibe: + vibe_descriptions = { + "clean": "clean & corporate - professional, lots of white space", + "friendly": "friendly & rounded - warm, approachable", + "minimal": "minimal & sharp - stark, modern", + "brutalist": "brutalist & weird - intentionally raw" + } + + col1, col2 = st.columns([2, 1]) + with col1: + st.markdown(f"**style:** {vibe_descriptions.get(project.style_vibe, project.style_vibe)}") + st.markdown(f"**primary color:** {project.primary_color}") + + with col2: + st.markdown(f""" +
+ """, unsafe_allow_html=True) +else: + st.caption("no style selected yet") + +st.markdown("---") + +# Architecture diagram +st.markdown("### how it all connects") +st.markdown(""" +Here's the big picture of what you're building: +""") + +# Simple text-based architecture diagram +architecture = f""" +``` + User + | + v + [ {project.framework.upper()} Frontend ] + | + | (API calls) + v + [ {project.database.upper()} ] + | + +-- {', '.join([n.name for n in project.nouns]) or 'your data'} + | + +-- User accounts {'('+project.auth_method+')' if project.auth_method else ''} + +Pages: {' -> '.join([p.path for p in project.pages[:5]]) or '/'} +``` +""" +st.markdown(architecture) + +st.markdown("---") + +# Export section +st.markdown("### export your config") +st.markdown("this is the structured definition of your project. it's what we'd use to generate code.") + +config = export_project_config() + +with st.expander("view full config (JSON)"): + st.code(config, language="json") + +col1, col2 = st.columns(2) + +with col1: + st.download_button( + label="download config.json", + data=config, + file_name=f"{project.project_name.lower().replace(' ', '-') if project.project_name else 'project'}-config.json", + mime="application/json" + ) + +with col2: + if st.button("copy to clipboard"): + st.code(config, language="json") + st.info("select and copy the config above") + +st.markdown("---") + +# What's next section +st.markdown("### what's next") + +if not progress["is_complete"]: + st.markdown("**finish your setup:**") + + if not progress["has_name"]: + if st.button("1. define your project"): + st.switch_page("pages/1_what_are_we_building.py") + elif not progress["has_nouns"]: + if st.button("2. add your data model (nouns)"): + st.switch_page("pages/2_what_are_the_nouns.py") + elif not progress["has_auth"]: + if st.button("3. configure authentication"): + st.switch_page("pages/3_how_do_people_get_in.py") + elif not progress["has_pages"]: + if st.button("4. define your pages"): + st.switch_page("pages/4_what_pages_exist.py") + elif not progress["has_style"]: + if st.button("5. pick your style"): + st.switch_page("pages/5_pick_a_vibe.py") +else: + st.markdown(""" + **your schtack is complete.** + + you now have: + - a clear project definition + - a data model you understand + - authentication configured + - pages mapped out + - a consistent style + + **what you can do now:** + + 1. **export the config** and use it as a reference while building + 2. **browse the component library** to see what pieces you'll need + 3. **start coding** - you know exactly what you're building + + the point was never to generate code for you. + the point was to make sure you understand what you're building + before you build it. + + now go build the thing. + """) + + col1, col2, col3 = st.columns(3) + + with col1: + if st.button("browse components"): + st.switch_page("pages/6_component_library.py") + + with col2: + if st.button("edit setup"): + st.switch_page("pages/1_what_are_we_building.py") + + with col3: + if st.button("start a new project", type="secondary"): + reset_project() + st.rerun() + +st.markdown("---") + +# Plain language summary +st.markdown("### in plain words") + +summary_parts = [] + +if project.project_name and project.project_description: + summary_parts.append(f"You're building **{project.project_name}** - {project.project_description}") + +if project.nouns: + noun_names = [n.name.lower() for n in project.nouns] + if len(noun_names) == 1: + summary_parts.append(f"It keeps track of **{noun_names[0]}**.") + else: + summary_parts.append(f"It keeps track of **{', '.join(noun_names[:-1])}** and **{noun_names[-1]}**.") + +if project.auth_method: + if project.auth_method == "none": + summary_parts.append("Anyone can use it without logging in.") + elif project.auth_method == "email": + summary_parts.append("People log in with their email and password.") + elif project.auth_method == "google": + summary_parts.append("People log in with their Google account.") + elif project.auth_method == "both": + summary_parts.append("People can log in with email/password or Google.") + +if project.pages: + public_pages = [p for p in project.pages if not p.requires_auth] + private_pages = [p for p in project.pages if p.requires_auth] + + if public_pages: + summary_parts.append(f"There are {len(public_pages)} public page(s) anyone can see.") + if private_pages: + summary_parts.append(f"There are {len(private_pages)} private page(s) that require login.") + +if project.framework and project.database: + summary_parts.append(f"It's built with **{project.framework}** and uses **{project.database}** for the database.") + +if summary_parts: + st.markdown("\n\n".join(summary_parts)) +else: + st.caption("complete your setup to see the summary") diff --git a/requirements.txt b/requirements.txt index 502d7d1a0d19..77e288293044 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1 @@ -altair -pandas -streamlit +streamlit>=1.28.0 diff --git a/streamlit_app.py b/streamlit_app.py index ac305b93bffd..5d06a1ebd494 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -1,38 +1,170 @@ -from collections import namedtuple -import altair as alt -import math -import pandas as pd -import streamlit as st +""" +fullshtack - the whole thing. no mystery meat. +A website builder that actually teaches you what you're building. """ -# Welcome to Streamlit! +import streamlit as st +import sys +from pathlib import Path -Edit `/streamlit_app.py` to customize this app to your heart's desire :heart: +# Add utils to path +sys.path.insert(0, str(Path(__file__).parent)) -If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community -forums](https://discuss.streamlit.io). +from utils.state import init_state, get_project, reset_project, get_progress_summary +from utils.content import HERO_TITLE, HERO_SUBTITLE, LANDING_PITCH -In the meantime, below is an example of what you can do with just a few lines of code: -""" +# Page config +st.set_page_config( + page_title="fullshtack", + page_icon="", + layout="wide", + initial_sidebar_state="collapsed" +) + +# Custom CSS for the vibe +st.markdown(""" + +""", unsafe_allow_html=True) + +# Initialize state +init_state() +project = get_project() +progress = get_progress_summary() + + +def show_landing_page(): + """The main pitch - what this is and why it's different.""" + + # Header + st.markdown(f'

{HERO_TITLE}

', unsafe_allow_html=True) + st.markdown(f'

{HERO_SUBTITLE}

', unsafe_allow_html=True) + + # The pitch + st.markdown(LANDING_PITCH) + + st.markdown("---") + + # CTA + col1, col2, col3 = st.columns([1, 2, 1]) + with col2: + if st.button("alright, let's build something", type="primary", use_container_width=True): + st.session_state.wizard_started = True + st.rerun() + + st.markdown("") + + if progress["has_name"]: + st.markdown("---") + st.markdown("#### or pick up where you left off") + if st.button(f"continue with: {project.project_name}", use_container_width=True): + st.switch_page("pages/1_what_are_we_building.py") + + +def show_progress_sidebar(): + """Show what's been set up in the sidebar.""" + with st.sidebar: + st.markdown("### your schtack") + + if not progress["has_name"]: + st.markdown("*nothing yet - let's start*") + return + + st.markdown(f"**{project.project_name}**") + + if project.project_description: + st.markdown(f"*{project.project_description[:100]}{'...' if len(project.project_description) > 100 else ''}*") + + st.markdown("---") + + # Progress checklist + steps = [ + ("project basics", progress["has_name"], "1_what_are_we_building"), + ("data model (nouns)", progress["has_nouns"], "2_what_are_the_nouns"), + ("authentication", progress["has_auth"], "3_how_do_people_get_in"), + ("pages & routing", progress["has_pages"], "4_what_pages_exist"), + ("styling & vibe", progress["has_style"], "5_pick_a_vibe"), + ] + + for step_name, is_done, page_name in steps: + status = "done" if is_done else "..." + icon = "" if is_done else "" + if st.button(f"{icon} {step_name}", key=f"nav_{page_name}", use_container_width=True): + st.switch_page(f"pages/{page_name}.py") + + st.markdown("---") + # Quick links + if progress["has_nouns"]: + if st.button("component library", use_container_width=True): + st.switch_page("pages/6_component_library.py") -with st.echo(code_location='below'): - total_points = st.slider("Number of points in spiral", 1, 5000, 2000) - num_turns = st.slider("Number of turns in spiral", 1, 100, 9) + if progress["is_complete"]: + if st.button("your schtack (dashboard)", use_container_width=True): + st.switch_page("pages/7_your_schtack.py") - Point = namedtuple('Point', 'x y') - data = [] + st.markdown("---") - points_per_turn = total_points / num_turns + if st.button("start over", type="secondary"): + reset_project() + st.rerun() - for curr_point_num in range(total_points): - curr_turn, i = divmod(curr_point_num, points_per_turn) - angle = (curr_turn + 1) * 2 * math.pi * i / points_per_turn - radius = curr_point_num / total_points - x = radius * math.cos(angle) - y = radius * math.sin(angle) - data.append(Point(x, y)) - st.altair_chart(alt.Chart(pd.DataFrame(data), height=500, width=500) - .mark_circle(color='#0068c9', opacity=0.5) - .encode(x='x:Q', y='y:Q')) +# Main app logic +if st.session_state.get('wizard_started') or progress["has_name"]: + show_progress_sidebar() + if st.session_state.get('wizard_started') and not progress["has_name"]: + st.switch_page("pages/1_what_are_we_building.py") + else: + show_landing_page() +else: + show_landing_page() diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 000000000000..bf9c2910588a --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +# fullshtack utilities diff --git a/utils/content.py b/utils/content.py new file mode 100644 index 000000000000..56159d4ba0b6 --- /dev/null +++ b/utils/content.py @@ -0,0 +1,292 @@ +""" +Plain language content for fullshtack. +No jargon. No mystery. Just explanations that make sense. +""" + +# The main pitch +HERO_TITLE = "fullshtack" +HERO_SUBTITLE = "the whole thing. no mystery meat." + +LANDING_PITCH = """ +**Look.** + +Those AI website builders are neat. Type a wish, get a website. Magic. + +But here's what they don't tell you: + +The moment you need to change something *real* - not the color, not the font, +the actual thing it *does* - you're standing in a codebase you've never seen before, +built by an AI that already forgot why it made the choices it made. + +You didn't build it. You ordered it. And now you're stuck reverse-engineering +your own project. + +--- + +**This is different.** + +We're not going to generate a website for you while you watch. + +We're going to build it *with* you. Step by step. In plain English. + +You'll know where everything is because you put it there. +You'll understand why it works because we explained it like humans, +not like a CS textbook trying to impress other CS textbooks. + +Is it slower than typing "make me an Airbnb clone"? +Yeah. + +Will you actually know what the hell you have at the end? +Also yeah. + +--- + +**This is for people who want to learn the shit by doing the shit.** + +Not by watching an AI do the shit and hoping you absorb it through vibes. +""" + +# Step explanations - what we're actually doing at each stage +STEP_EXPLANATIONS = { + "project": { + "title": "what are we building?", + "subtitle": "let's start with the basics, in normal words", + "explanation": """ +Before we write a single line of code, let's just talk about what you're making. + +Not "define your project requirements" - just... what is it? +A tool for tracking something? A site where people can do a thing? +An app that connects people who have X with people who need X? + +Tell me like you'd tell a friend who asked "so what are you working on?" + """, + "why_this_matters": """ +Here's why we do this first: every decision that comes after flows from this. + +If you're building a blog, you need posts and maybe comments. +If you're building a marketplace, you need sellers, buyers, and listings. +If you're building a tool, you need users and whatever they're tracking. + +The clearer you are now, the less "wait, I need to redo everything" later. + """ + }, + + "nouns": { + "title": "what are the nouns?", + "subtitle": "the things your app keeps track of", + "explanation": """ +Every app is basically: people doing things with stuff. + +Let's figure out what "stuff" your app has. + +- Users (obviously, almost always) +- But what else? Products? Posts? Messages? Properties? Tasks? + +Just list the nouns. The things. We'll figure out how they connect in a sec. + +*Tech translation: this is "data modeling" or "defining your schema" - +but those words don't help you think clearly about what you actually need.* + """, + "why_this_matters": """ +Your database is just a filing cabinet for these nouns. + +Each noun becomes a "table" - a spreadsheet, basically. +Each thing you track about that noun becomes a "column" in the spreadsheet. + +A User might have: email, name, password (encrypted), when they signed up. +A Post might have: title, content, who wrote it, when. + +This is the skeleton of your app. Everything else hangs off of it. + """ + }, + + "auth": { + "title": "how do people get in?", + "subtitle": "the login situation", + "explanation": """ +Does your app need user accounts? + +If yes: how should people log in? + +- **Just email & password** - the classic +- **Google/social login** - one click, uses their existing account +- **Both** - let them choose +- **No login needed** - it's a public tool, no accounts required + +*Tech translation: this is "authentication" - but that word makes it sound +more complicated than "checking if someone is who they say they are."* + """, + "why_this_matters": """ +Auth touches everything. It's not just the login page. + +It's: who can see what? Who can edit what? What happens when you're not logged in? + +We set this up properly now so you're not hacking it in later and creating +security holes because you're tired and it's 2am. + """ + }, + + "pages": { + "title": "what pages exist?", + "subtitle": "the screens people actually see", + "explanation": """ +Let's map out your app like a house. + +What rooms are there? Front door (home page), obviously. Then what? + +- A page where people sign up? +- A page where they see their stuff? +- A page where they create new stuff? +- A settings page? + +Just list them. We'll worry about what's on each page in a minute. + +*Tech translation: this is "routing" - but it's really just +"what URLs exist and what do you see when you go there."* + """, + "why_this_matters": """ +Your pages are the skeleton of the user experience. + +Each page usually does one main thing: +- Show a list of things +- Show one thing in detail +- Let you create/edit a thing +- Let you configure settings + +Knowing your pages means knowing what components you'll need. +And which nouns show up where. + """ + }, + + "styling": { + "title": "pick a vibe", + "subtitle": "how should this thing look?", + "explanation": """ +We're not going to pretend CSS isn't annoying. It is. + +But picking a general direction now saves hours of "why doesn't this look right" later. + +Pick a vibe: + +- **Clean & Corporate** - Lots of white space, professional, trustworthy +- **Friendly & Rounded** - Softer edges, warmer colors, approachable +- **Minimal & Sharp** - Less is more, stark contrasts, modern +- **Brutalist & Weird** - Intentionally raw, anti-design design + +*Tech translation: this determines your CSS framework, spacing scale, +border radius defaults, and color palette - but you don't need to know that.* + """, + "why_this_matters": """ +A consistent visual language makes your app feel "finished" even when it's not. + +We'll set up design tokens (fancy word for "the handful of colors, +fonts, and sizes you'll reuse everywhere") so everything matches automatically. + +You can always change it later. But having a starting point beats +staring at an unstyled page wondering where to begin. + """ + } +} + +# Component explanations +COMPONENT_EXPLANATIONS = { + "button": { + "what_it_is": "A clickable thing that does something when you click it.", + "what_it_needs": "Text to display, and what should happen when clicked.", + "common_uses": "Submit forms, trigger actions, navigate somewhere.", + "gotchas": "Buttons inside forms behave differently than buttons outside forms." + }, + "form": { + "what_it_is": "A container for inputs that collects data and sends it somewhere.", + "what_it_needs": "Input fields, a submit button, and somewhere to send the data.", + "common_uses": "Sign up, log in, create/edit anything, search.", + "gotchas": "Always validate on the server too. Never trust the browser alone." + }, + "input": { + "what_it_is": "A box where someone types something.", + "what_it_needs": "A label (what to type), a name (how to identify it), and validation (what counts as valid).", + "common_uses": "Email, password, names, search queries, any text.", + "gotchas": "Use the right type (email, password, number) - it changes the keyboard on mobile." + }, + "card": { + "what_it_is": "A contained box that groups related content together.", + "what_it_needs": "Content to display. Usually has a consistent structure (image, title, description, action).", + "common_uses": "Displaying items in a list/grid - products, posts, users, anything.", + "gotchas": "Keep cards in a grid consistent. Same info in the same places." + }, + "nav": { + "what_it_is": "The menu that lets people move between pages.", + "what_it_needs": "Links to your pages. Usually shows different options for logged-in vs logged-out users.", + "common_uses": "Header navigation, sidebar navigation, mobile hamburger menu.", + "gotchas": "Should clearly show where you currently are." + }, + "modal": { + "what_it_is": "A popup that appears over the page, demanding attention.", + "what_it_needs": "Content to display, a way to close it, and a trigger to open it.", + "common_uses": "Confirmations ('are you sure?'), quick forms, important alerts.", + "gotchas": "Don't overuse. If everything is a modal, nothing is." + }, + "table": { + "what_it_is": "Data organized in rows and columns. A spreadsheet view.", + "what_it_needs": "Column headers and row data. Often needs sorting and filtering.", + "common_uses": "Admin panels, data-heavy views, comparison features.", + "gotchas": "Terrible on mobile. Consider card layouts for smaller screens." + }, + "list": { + "what_it_is": "Items displayed one after another, vertically.", + "what_it_needs": "Items to display. Usually each item is clickable.", + "common_uses": "Messages, notifications, simple item displays.", + "gotchas": "Long lists need pagination or infinite scroll. Don't load 10,000 items." + } +} + + +# Error messages in plain language +ERROR_MESSAGES = { + "empty_name": "You gotta call it something. What's the working title?", + "empty_description": "Even a rough idea helps. What does it do, in a sentence?", + "no_nouns": "Every app tracks *something*. What are the things in yours?", + "no_pages": "People need somewhere to go. What's the first page they see?", + "duplicate_noun": "You already have one of those. Did you mean something different?", + "invalid_path": "Page paths should look like /something or /something/else - no spaces, lowercase.", +} + + +# Success messages +SUCCESS_MESSAGES = { + "project_saved": "got it. we know what we're building now.", + "nouns_saved": "nice. your data model makes sense.", + "auth_saved": "login situation: handled.", + "pages_saved": "your app has a map now.", + "style_saved": "looking good. well, it will be.", + "all_done": "nice. you have a working app skeleton. you know where everything is. nothing weird hiding in the closet." +} + + +# AI explanation templates +def explain_like_human(tech_concept: str) -> str: + """Translate tech jargon to normal words.""" + translations = { + "api endpoint": "a spot where your app sends/receives data", + "database query": "asking your database for specific information", + "authentication": "checking if someone is who they say they are", + "authorization": "checking if someone is allowed to do what they're trying to do", + "middleware": "code that runs between 'request comes in' and 'response goes out'", + "state management": "keeping track of what's happening in your app right now", + "component": "a reusable piece of your interface", + "props": "the settings/data you pass into a component", + "hook": "a way to tap into React's features in a function", + "schema": "the structure of your data - what fields exist and what type they are", + "migration": "a script that changes your database structure", + "orm": "code that lets you talk to your database using your programming language instead of SQL", + "rest api": "a standard way to structure your app's data endpoints", + "crud": "create, read, update, delete - the four basic things you do with data", + "jwt": "a secure token that proves someone is logged in", + "session": "remembering who someone is while they're using your app", + "environment variables": "secret settings that shouldn't be in your code", + "deployment": "putting your app on the internet so people can use it", + "ssl/https": "encryption that keeps data safe between the user and your server", + "cors": "browser security that controls which sites can talk to your app", + } + + return translations.get(tech_concept.lower(), f"'{tech_concept}' - honestly, look this one up, it's context-dependent") diff --git a/utils/state.py b/utils/state.py new file mode 100644 index 000000000000..ba611a16ce45 --- /dev/null +++ b/utils/state.py @@ -0,0 +1,158 @@ +""" +Project state management for fullshtack. +Keeps track of everything the user has set up, in plain terms. +""" +import streamlit as st +from dataclasses import dataclass, field +from typing import Dict, List, Optional +import json + + +@dataclass +class Noun: + """A thing you're keeping track of in your app.""" + name: str + description: str + fields: List[Dict[str, str]] = field(default_factory=list) + relationships: List[Dict[str, str]] = field(default_factory=list) + + +@dataclass +class Page: + """A page in your app.""" + name: str + path: str + description: str + requires_auth: bool = False + connected_nouns: List[str] = field(default_factory=list) + + +@dataclass +class ProjectState: + """The whole picture of what you're building.""" + # Step 1: What are we building? + project_name: str = "" + project_description: str = "" + project_type: str = "" # 'app', 'site', 'tool', etc. + + # Step 2: What are the nouns? + nouns: List[Noun] = field(default_factory=list) + + # Step 3: How do people get in? + auth_method: str = "" # 'email', 'google', 'both', 'none' + auth_features: List[str] = field(default_factory=list) # 'password_reset', 'remember_me', etc. + + # Step 4: What pages exist? + pages: List[Page] = field(default_factory=list) + + # Step 5: Pick a vibe + style_vibe: str = "" # 'clean', 'friendly', 'brutalist', 'minimal' + primary_color: str = "#0068c9" + + # Stack choices + framework: str = "nextjs" # 'nextjs', 'sveltekit', 'astro' + database: str = "supabase" # 'supabase', 'planetscale', 'sqlite' + + # Progress tracking + current_step: int = 0 + completed_steps: List[int] = field(default_factory=list) + + +def init_state(): + """Set up the project state if it doesn't exist.""" + if 'project' not in st.session_state: + st.session_state.project = ProjectState() + if 'wizard_started' not in st.session_state: + st.session_state.wizard_started = False + return st.session_state.project + + +def get_project() -> ProjectState: + """Get the current project state.""" + return init_state() + + +def update_project(**kwargs): + """Update the project with new values.""" + project = get_project() + for key, value in kwargs.items(): + if hasattr(project, key): + setattr(project, key, value) + + +def mark_step_complete(step: int): + """Mark a wizard step as done.""" + project = get_project() + if step not in project.completed_steps: + project.completed_steps.append(step) + project.current_step = step + 1 + + +def reset_project(): + """Start over from scratch.""" + st.session_state.project = ProjectState() + st.session_state.wizard_started = False + + +def get_progress_summary() -> Dict: + """Get a plain-language summary of what's been set up.""" + project = get_project() + + summary = { + "has_name": bool(project.project_name), + "has_nouns": len(project.nouns) > 0, + "has_auth": bool(project.auth_method), + "has_pages": len(project.pages) > 0, + "has_style": bool(project.style_vibe), + "is_complete": len(project.completed_steps) >= 5 + } + + return summary + + +def export_project_config() -> str: + """Export the project setup as a config that could generate code.""" + project = get_project() + + config = { + "project": { + "name": project.project_name, + "description": project.project_description, + "type": project.project_type + }, + "stack": { + "framework": project.framework, + "database": project.database + }, + "data_model": { + "entities": [ + { + "name": noun.name, + "description": noun.description, + "fields": noun.fields, + "relationships": noun.relationships + } + for noun in project.nouns + ] + }, + "auth": { + "method": project.auth_method, + "features": project.auth_features + }, + "pages": [ + { + "name": page.name, + "path": page.path, + "description": page.description, + "requires_auth": page.requires_auth, + "uses_data": page.connected_nouns + } + for page in project.pages + ], + "styling": { + "vibe": project.style_vibe, + "primary_color": project.primary_color + } + } + + return json.dumps(config, indent=2)