diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..b987073 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.29.3" +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f84cafd --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Run with Dart", + "request": "launch", + "type": "dart", + "program": "web/main.dart" + }, + { + "name": "Run with webdev (auto-refresh)", + "request": "launch", + "type": "dart", + "program": "web/main.dart", + "args": [ + "--web-renderer", + "html", + "--web-port", + "8080" + ], + "preLaunchTask": "webdev-serve" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e560150 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": ".fvm/versions/3.29.3" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..90ba247 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "webdev-serve", + "type": "shell", + "command": "webdev serve --auto refresh", + "isBackground": true, + "problemMatcher": { + "owner": "dart", + "pattern": { + "regexp": ".", + "file": 1, + "location": 2, + "message": 3 + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^\\[INFO\\] Serving", + "endsPattern": "^\\[INFO\\] Succeeded" + } + } + } + ] +} \ No newline at end of file diff --git a/docs/builders/README.md b/docs/builders/README.md deleted file mode 100644 index c3ca300..0000000 --- a/docs/builders/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Flutter AI Builders Circle ✨ - -## About Us - -The Flutter AI Builders Circle is an open-source, volunteer-led community focused on integrating AI capabilities into Flutter applications. We bring together Flutter developers interested in building agentic apps using cutting-edge technologies like Gemini, Whisper, Multimodal Content Processing (MCP), and Retrieval-Augmented Generation (RAG). - -## Our Mission 🎯 - -We aim to: - -- Create accessible resources for Flutter developers to incorporate AI into their apps -- Explore and share best practices for responsible AI implementation -- Build a supportive community for experimentation and learning -- Develop open-source starter kits and reference implementations - -## What We Do πŸ“¦ - -- **Share Knowledge**: Technical articles, code examples, and implementation guides -- **Build Tools**: Open-source packages and utilities to simplify AI integration -- **Provide Templates**: Ready-to-use starter projects for common AI tasks -- **Foster Collaboration**: Connect developers working on similar challenges - -## Join Us - -**Coming Soon** - -We're currently preparing our community platforms. Join us to collaborate, learn, and build the future of AI-powered Flutter apps. - -## Contributors - -**Coming Soon** - -## Want to Contribute? πŸ”— - -We welcome contributions from Flutter developers of all experience levels! Whether you're an AI expert or just getting started, there's a place for you in our community. - -PRs welcome - watch this space for contribution guidelines. \ No newline at end of file diff --git a/docs/starters/README.md b/docs/starters/README.md deleted file mode 100644 index f90e247..0000000 --- a/docs/starters/README.md +++ /dev/null @@ -1,113 +0,0 @@ -# πŸ“¦ Flutter AI Builders Circle – Starters - -Welcome to the **Starters Hub** of the Flutter AI Builders Circle. - -This section offers high-quality, community-vetted resources to help you explore and build **agentic apps** using Flutter and AI β€” from basic LLM integrations to more advanced workflows using RAG, MCP, and Whisper. - -> 🧠 We believe in learning by doing β€” and starting with official sources. All links below come from official documentation or deeply trusted platforms. - ---- - -## 🧩 Core Concepts You Should Know - -### What is an Agentic App? - -- 🧠 [LangChain: Introduction](https://python.langchain.com/docs/introduction/) – Learn the foundations of LangChain and its modular architecture for building LLM-powered agents. -- πŸ“˜ [What is Agentic AI? (IBM)](https://www.ibm.com/think/topics/agentic-ai#:~:text=Agentic%20AI%20solutions%20can%20be,predictive%20analytics%20and%20execute%20trades.) – Overview of how agentic AI differs from traditional AI, with real-world examples. -- πŸ” [Introducing the Model Context Protocol (Anthropic)](https://www.anthropic.com/news/model-context-protocol) – Learn how MCP standardizes interactions between LLMs and tools. -- πŸ“š [Amazon AWS: What is RAG (Retrieval-Augmented Generation)?](https://aws.amazon.com/what-is/retrieval-augmented-generation/) – A simple primer on Retrieval-Augmented Generation and how it improves model accuracy. -- πŸŽ₯ [Observable Flutter #59 – Agentic Apps with Flutter](https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX) – Craig Labenz and Andrew Brogdon walk through agentic patterns, Colorist, and Firebase Genkit. - ---- - -## πŸ”— Official Docs & Tools Worth Exploring - -### LLM Integration (Gemini, OpenAI, Claude) - -- **Gemini API + Flutter (Colorist Codelab)** – Beginner/Intermediate. Learn how to integrate Gemini into a Flutter app using Firebase extensions and function calling. - πŸ‘‰ [Colorist Codelab](https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist) - -- **OpenAI API Docs** – Beginner. Covers completions, chat, and function calling for GPT models. - πŸ‘‰ [OpenAI Platform Docs](https://platform.openai.com/docs) - -- **dart_openai (Community SDK)** – Intermediate. A Dart package that simplifies using OpenAI APIs within Flutter projects. - πŸ‘‰ [dart_openai on pub.dev](https://pub.dev/packages/dart_openai) - -- **Anthropic Claude API** – Beginner. Official Claude API docs with example request/response flows. - πŸ‘‰ [Claude API Overview](https://docs.anthropic.com/claude/docs) - -### Speech-to-Text (Whisper) - -- **OpenAI Whisper API** – Beginner. Transcribe audio to text using OpenAI’s hosted Whisper model. - πŸ‘‰ [Whisper Speech-to-Text](https://platform.openai.com/docs/guides/speech-to-text) - -- **flutter_whisper.cpp** – Advanced. Community-maintained Flutter app running Whisper on-device via Rust FFI and whisper.cpp. - πŸ‘‰ [flutter_whisper.cpp on GitHub](https://github.com/lyledean1/flutter_whisper.cpp) - -### Firebase Vector Search & RAG - -- **Firestore Vector Search (Extension)** – Intermediate. Store and retrieve vector embeddings in Firestore for building RAG pipelines. - πŸ‘‰ [Firebase Vector Search Docs](https://extensions.dev/extensions/googlecloud/firestore-vector-search) - -- **Firebase Genkit** – Advanced. A backend framework for building AI-driven workflows with Gemini, designed for Cloud Functions. - πŸ‘‰ [Firebase Genkit Docs](https://firebase.google.com/docs/genkit) - -### MCP (Model Context Protocol) - -- **Model Context Protocol Overview** – Advanced. MCP is an open protocol to connect LLMs with tools and data providers. - πŸ‘‰ [Model Context Protocol Site](https://modelcontextprotocol.io/introduction) - -### Local LLMs (Ollama) - -- **Ollama** – Intermediate. Easily run LLMs like LLaMA or Mistral locally and serve them via HTTP. - πŸ‘‰ [Ollama GitHub](https://github.com/ollama/ollama) - -### Agentic Patterns - -- **Function Calling (OpenAI)** – Intermediate. Enables models to call structured functions from your app. - πŸ‘‰ [Function Calling Docs](https://platform.openai.com/docs/guides/function-calling) - -- **LLM Agents (Prompt Engineering Guide)** – Advanced. Explore ReAct and other reasoning/action agent patterns. - πŸ‘‰ [Prompt Engineering Agents Guide](https://www.promptingguide.ai/techniques/react) - -### Codelabs, Templates, and Examples - -- **Colorist (Gemini + Flutter)** – Intermediate. Gemini-powered Flutter app that updates the UI based on natural language prompts. - πŸ‘‰ [Colorist Codelab](https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist) - -- **Gemini API Cookbook (GitHub)** – Intermediate/Advanced. Google’s official examples for calling Gemini API across languages including Dart. - πŸ‘‰ [Gemini Cookbook GitHub](https://github.com/google-gemini/cookbook) - -### Dart AI Libraries - -- **LangChain.dart** – Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more. - πŸ‘‰ [LangChain Dart on pub.dev](https://pub.dev/packages/langchain) - ---- - -## 🧠 Want to Deepen Your Understanding? - -- πŸŽ₯ [Agentic Apps with Flutter (Observable Flutter #59)](https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX) -- πŸ’¬ [Gemini Function Calling Walkthrough (Google I/O)](https://ai.google.dev/gemini-api/docs/function-calling) - ---- - -## 🀝 Contribute or Join the Circle - -We’re building these kits and docs as a community. If you’d like to: - -- Submit your own starter -- Add a resource to this list -- Help write docs or translate examples - -πŸ‘‰ **Open a PR or stay tuned for our Slack/Discord β€” coming soon!** - ---- - -## πŸ§ͺ Coming Next - -Stay tuned at **[ai.fluttercommunity.dev](https://ai.fluttercommunity.dev)** - ---- - -🌱 *This space is volunteer-built with ❀️ β€” we aim to make agentic Flutter development more accessible for everyone.* \ No newline at end of file diff --git a/lib/src/app/external_links.dart b/lib/src/app/external_links.dart index e11e24e..ba3623d 100644 --- a/lib/src/app/external_links.dart +++ b/lib/src/app/external_links.dart @@ -1,6 +1,11 @@ enum ExternalLink { youTubePlaylist('https://www.youtube.com/playlist?list=PL4dBIh1xps-HIYvaEIbLWHZqt_WGBfpx3'), youTubeVibeCoding('https://www.youtube.com/playlist?list=PL4dBIh1xps-GLrzqijiTRuzdtxP3bfqsQ'), + youTubeAgenticQA('https://www.youtube.com/live/XRuKywXD7zM?si=KHjiSKayFwuFz7GH'), + youTubeFlutterCommunity('https://www.youtube.com/@FlutterCommunity'), + youTubeJhinLeeMCPHumpdayQA('https://www.youtube.com/live/1U-zd6MYrOA?si=HdWVE-H8zDNOL0Sx&t=1794'), + geminiFunctionCallingDocs( + 'https://ai.google.dev/gemini-api/docs/function-calling?example=meeting'), forumCategory('https://forum.itsallwidgets.com/c/ai/46'), forumChat('https://forum.itsallwidgets.com/chat/c/fc-ai-circle/40'), surveyCommunity('https://forms.gle/JrC11pxRz6TLyB2L9'), diff --git a/lib/src/app/website.dart b/lib/src/app/website.dart index 93d32d3..a244a43 100644 --- a/lib/src/app/website.dart +++ b/lib/src/app/website.dart @@ -1,6 +1,8 @@ import 'package:fc_ai_circle/src/pages/builders_page.dart'; import 'package:fc_ai_circle/src/pages/home_page.dart'; import 'package:fc_ai_circle/src/pages/starters_page.dart'; +import 'package:fc_ai_circle/src/pages/privacy_policy_page.dart'; +import 'package:fc_ai_circle/src/pages/code_of_conduct_page.dart'; import 'package:jaspr/browser.dart'; import 'package:jaspr_router/jaspr_router.dart'; @@ -18,14 +20,18 @@ class _SiteState extends State { children: [ link(href: '/css/fontawesome.css', rel: 'stylesheet'), link(href: '/css/brands.css', rel: 'stylesheet'), - // Style(styles: resetStyles) + link(href: '/css/clickable-cards.css', rel: 'stylesheet'), + script(src: '/js/scroll.js', attributes: {'defer': 'true'}, []), + script(src: '/js/clickable-cards.js', attributes: {'defer': 'true'}, []), ], ); yield Router( routes: [ ...HomePage.route(), - StartersPage.route(), - BuildersPage.route(), + ...StartersPage.route(), + ...BuildersPage.route(), + ...PrivacyPolicyPage.route(), + ...CodeOfConductPage.route(), ], ); } diff --git a/lib/src/components/content_section.dart b/lib/src/components/content_section.dart new file mode 100644 index 0000000..51b94e7 --- /dev/null +++ b/lib/src/components/content_section.dart @@ -0,0 +1,148 @@ +import 'package:jaspr/browser.dart'; + +class ContentSection extends StatelessComponent { + const ContentSection({ + super.key, + required this.title, + this.id, + this.emoji, + this.description, + this.contentPadding = true, + this.elevated = false, + required this.children, + }); + + final String title; + final String? id; + final String? emoji; + final String? description; + final bool contentPadding; + final bool elevated; + final List children; + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: id, + classes: ['content-section', if (elevated) 'elevated-section'].join(' '), + attributes: {'role': 'region', 'aria-label': title}, + [ + div( + classes: 'container', + [ + h2( + classes: 'section-title', + [ + if (emoji != null) + span( + attributes: {'aria-hidden': 'true'}, + [text('$emoji ')], + ), + text(title), + ], + ), + if (description != null) + p( + classes: 'section-description', + [text(description!)], + ), + div( + classes: contentPadding ? 'section-content' : '', + children, + ), + ], + ), + ], + ); + } +} + +class ContentCard extends StatelessComponent { + const ContentCard({ + super.key, + required this.children, + this.elevated = false, + this.animateCta = false, + }); + + final List children; + final bool elevated; + final bool animateCta; + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: [ + 'content-card', + if (elevated) 'elevated', + if (animateCta) 'animate-cta', + ].where((c) => c.isNotEmpty).join(' '), + children, + ); + } +} + +class EmojiCallout extends StatelessComponent { + const EmojiCallout({ + super.key, + required this.emoji, + required this.children, + this.blurred = true, + }); + + final String emoji; + final List children; + final bool blurred; + + @override + Iterable build(BuildContext context) sync* { + // Get a description of the emoji for screen readers + final String emojiDescription = _getEmojiDescription(emoji); + + yield div( + classes: ['emoji-callout', if (blurred) 'blurred'].join(' '), + [ + span( + classes: 'emoji', + attributes: {'aria-hidden': 'true'}, // Hide from screen readers as we'll describe it + [text(emoji)], + ), + // Add hidden text for screen readers + span( + attributes: { + 'class': 'sr-only', + 'role': 'presentation', + }, + [text(emojiDescription)], + ), + div(classes: 'emoji-content', children), + ], + ); + } + + // Helper method to get a description of common emojis + String _getEmojiDescription(String emoji) { + switch (emoji) { + case '🧠': + return 'Brain emoji representing knowledge or thinking'; + case 'πŸš€': + return 'Rocket emoji representing launch or fast progress'; + case '🌟': + return 'Star emoji representing excellence or highlights'; + case '🎯': + return 'Target emoji representing goals or mission'; + case 'πŸ“¦': + return 'Package emoji representing products or deliverables'; + case '🧰': + return 'Toolbox emoji representing toolkit or resources'; + case '✨': + return 'Sparkles emoji representing new features or excitement'; + case 'πŸ”—': + return 'Link emoji representing connections or contributions'; + case 'πŸ§ͺ': + return 'Test tube emoji representing experiments or tests'; + default: + return 'Emoji'; + } + } +} diff --git a/lib/src/components/feature_card.dart b/lib/src/components/feature_card.dart new file mode 100644 index 0000000..4776df1 --- /dev/null +++ b/lib/src/components/feature_card.dart @@ -0,0 +1,42 @@ +import 'package:jaspr/browser.dart'; + +/// A card displaying a feature with title, description and optional emoji. +/// +/// Used to display key capabilities or offerings in a uniform grid layout. +class FeatureCard extends StatelessComponent { + const FeatureCard({ + super.key, + required this.title, + required this.description, + this.isHighlighted = false, + this.emoji, + }); + + /// The title displayed at the top of the card + final String title; + + /// A short description of the feature + final String description; + + /// Whether to apply highlighted styling to this card + final bool isHighlighted; + + /// An optional emoji to display before the title + final String? emoji; + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: isHighlighted ? 'feature-card highlighted' : 'feature-card', + attributes: {'role': 'listitem'}, + [ + h3([ + if (emoji != null) + span(classes: 'feature-emoji', attributes: {'aria-hidden': 'true'}, [text('$emoji ')]), + text(title), + ]), + p([text(description)]), + ], + ); + } +} diff --git a/lib/src/components/footer.dart b/lib/src/components/footer.dart index 608f8dd..dd9b10b 100644 --- a/lib/src/components/footer.dart +++ b/lib/src/components/footer.dart @@ -3,6 +3,9 @@ import 'package:jaspr/browser.dart'; import 'package:jaspr_router/jaspr_router.dart'; import 'package:fc_ai_circle/src/pages/builders_page.dart'; import 'package:fc_ai_circle/src/pages/starters_page.dart'; +import 'package:fc_ai_circle/src/pages/privacy_policy_page.dart'; +import 'package:fc_ai_circle/src/pages/code_of_conduct_page.dart'; +import 'dart:js' as js; class Footer extends StatelessComponent { @override @@ -13,8 +16,8 @@ class Footer extends StatelessComponent { FooterColumn( title: 'Community', links: [ - (path: '#', label: 'What is Agentic Flutter?'), - (path: '#', label: 'Take the contributors survey'), + (path: ExternalLink.youTubeAgenticQA.url, label: 'What is Agentic Flutter?'), + (path: ExternalLink.surveyContributors.url, label: 'Take the contributors survey'), ], ), FooterColumn( @@ -28,8 +31,8 @@ class Footer extends StatelessComponent { FooterColumn( title: 'Legal', links: [ - (path: '#', label: 'Privacy Policy'), - (path: '#', label: 'Code of Conduct'), + (path: PrivacyPolicyPage.path, label: 'Privacy Policy'), + (path: CodeOfConductPage.path, label: 'Code of Conduct'), ], ), ]), @@ -88,11 +91,37 @@ class FooterColumn extends StatelessComponent { ul([ for (var link in links) // li([ - a( - href: link.path, - onClick: () => Router.of(context).push(link.path), - [text(link.label)], - ) + if (link.path == '#coming-soon') + span( + classes: 'footer-link coming-soon-link', + attributes: { + 'data-tooltip': 'Coming soon', + 'aria-label': '${link.label} (Coming soon)' + }, + [text(link.label)], + ) + else if (link.path.startsWith('http')) + a( + href: link.path, + target: Target.blank, + attributes: { + 'rel': 'noopener noreferrer', + 'aria-label': link.label, + }, + [text(link.label)], + ) + else + a( + href: link.path, + onClick: () { + // Push to the new route + Router.of(context).push(link.path); + // Force scroll to top with delay to ensure it happens after navigation + js.context.callMethod( + 'eval', ['setTimeout(function() { window.scrollTo(0, 0); }, 10);']); + }, + [text(link.label)], + ) ]), ]), ]); diff --git a/lib/src/components/navbar.dart b/lib/src/components/navbar.dart index 2d3e9da..70a15ff 100644 --- a/lib/src/components/navbar.dart +++ b/lib/src/components/navbar.dart @@ -15,37 +15,76 @@ class Navbar extends StatefulComponent { } class _NavbarState extends State { + bool _isMenuOpen = false; + + void _toggleMenu() { + setState(() { + _isMenuOpen = !_isMenuOpen; + }); + } + @override Iterable build(BuildContext context) sync* { yield nav( id: 'navbar', classes: 'navbar', [ - div(classes: 'container', [ - a( - href: HomePage.path, - onClick: () => Router.of(context).push(HomePage.path), - classes: 'navbar-brand', - [ - img( - src: '/images/logo_1x.png', - alt: 'Flutter Community AI Circle', - ) - ], - ), - div( - classes: 'nav-links', - [ - _NavItem(path: '/', label: 'Home'), - _NavItem(path: StartersPage.path, label: 'Starters'), - _NavItem(path: BuildersPage.path, label: 'Builders'), - _NavItem(link: ExternalLink.youTubePlaylist, label: 'YouTube'), - _NavItem(link: ExternalLink.socialGitHub, label: 'GitHub'), - _NavItem(link: ExternalLink.forumCategory, label: 'Forum'), - ], - ), - TakeSurvey(), - ]), + div( + classes: 'container', + [ + // Top row with logo and mobile menu toggle + div( + classes: 'navbar-top-row', + [ + a( + href: HomePage.path, + onClick: () => Router.of(context).push(HomePage.path), + classes: 'navbar-brand', + [ + img( + src: '/images/logo_1x.png', + alt: 'Flutter Community AI Circle', + ) + ], + ), + // Mobile menu toggle button + div( + classes: 'mobile-menu-toggle', + events: { + 'click': (event) => _toggleMenu(), + }, + [ + span([]), + span([]), + span([]), + ], + ), + ], + ), + // Navigation links with proper wrapper + div( + classes: 'nav-menu-container ${_isMenuOpen ? 'open' : ''}', + [ + div( + classes: 'nav-links', + [ + _NavItem(path: '/', label: 'Home'), + _NavItem(path: StartersPage.path, label: 'Starters'), + _NavItem(path: BuildersPage.path, label: 'Builders'), + _NavItem(link: ExternalLink.youTubePlaylist, label: 'YouTube'), + _NavItem(link: ExternalLink.socialGitHub, label: 'GitHub'), + _NavItem(link: ExternalLink.forumCategory, label: 'Forum'), + ], + ), + // Survey button inside nav container for mobile + div( + classes: 'navbar-cta', + [TakeSurvey()], + ), + ], + ), + ], + ), ], ); } @@ -64,13 +103,11 @@ class _NavItem extends StatelessComponent { @override Iterable build(BuildContext context) sync* { - // TODO: selected state? final selected = window.location.pathname == path; yield a( classes: [ 'nav-link', - if (selected) // - 'nav-selected', + if (selected) 'nav-selected', ].join(' '), href: path ?? link!.url, target: link != null ? Target.blank : null, diff --git a/lib/src/components/page_hero.dart b/lib/src/components/page_hero.dart new file mode 100644 index 0000000..6cc4238 --- /dev/null +++ b/lib/src/components/page_hero.dart @@ -0,0 +1,42 @@ +import 'package:jaspr/browser.dart'; + +class PageHero extends StatelessComponent { + const PageHero({ + super.key, + required this.title, + this.description, + this.callout, + }); + + final String title; + final String? description; + final Component? callout; + + @override + Iterable build(BuildContext context) sync* { + yield section( + classes: 'hero', + attributes: {'role': 'banner'}, + [ + div(classes: 'container', [ + div( + classes: 'hero-content', + [ + h1( + classes: 'page-title', + [ + span( + classes: 'title-text', + [text(title)], + ), + ], + ), + if (description != null) p(classes: 'hero-description', [text(description!)]), + if (callout != null) callout!, + ], + ), + ]), + ], + ); + } +} diff --git a/lib/src/components/resource_category.dart b/lib/src/components/resource_category.dart new file mode 100644 index 0000000..bd766ab --- /dev/null +++ b/lib/src/components/resource_category.dart @@ -0,0 +1,127 @@ +import 'package:jaspr/browser.dart'; +import 'package:fc_ai_circle/src/components/resource_link.dart'; + +enum CardVariant { + standard, + highlighted, + compact, + blueGlow, + greenGlow, + orangeGlow, +} + +enum CategoryAccent { + none, + purple, + blue, + green, + orange, +} + +class ResourceCategory extends StatelessComponent { + const ResourceCategory({ + super.key, + required this.title, + required this.resources, + this.accent = CategoryAccent.none, + this.useGrid = false, + this.useHorizontalScroll = false, + this.useMasonryGrid = false, + }); + + final String title; + final List resources; + final CategoryAccent accent; + final bool useGrid; + final bool useHorizontalScroll; + final bool useMasonryGrid; + + String get accentClassName { + switch (accent) { + case CategoryAccent.none: + return ''; + case CategoryAccent.purple: + return 'purple-accent'; + case CategoryAccent.blue: + return 'blue-accent'; + case CategoryAccent.green: + return 'green-accent'; + case CategoryAccent.orange: + return 'orange-accent'; + } + } + + Component renderScrollableContainer(String containerId) { + return div( + classes: 'scrollable-container dynamic-scroll-buttons', + attributes: { + 'id': containerId, + }, + [ + div( + classes: 'scroll-nav-button scroll-nav-left', + attributes: { + 'aria-label': 'Scroll left', + 'style': 'display: none; background-color: var(--primary-color); color: white;', + 'title': 'Scroll to see previous items', + }, + [text('←')], + ), + div( + classes: 'scroll-nav-button scroll-nav-right', + attributes: { + 'aria-label': 'Scroll right', + 'style': 'display: none; background-color: var(--primary-color); color: white;', + 'title': 'Scroll to see more items', + }, + [text('β†’')], + ), + div( + classes: 'scrollable-items', + resources, + ), + ], + ); + } + + Component renderResourceList() { + return div( + classes: 'resource-list', + resources, + ); + } + + Component renderResourceGrid() { + return div( + classes: 'resource-grid', + resources, + ); + } + + @override + Iterable build(BuildContext context) sync* { + final categoryClasses = ['resource-category']; + final String containerId = 'scrollable-${DateTime.now().millisecondsSinceEpoch}'; + + if (accentClassName.isNotEmpty) { + categoryClasses.add(accentClassName); + } + + yield div( + classes: categoryClasses.join(' '), + attributes: { + 'role': 'region', + 'aria-label': title, + }, + [ + h3(classes: 'category-title', [text(title)]), + if (useHorizontalScroll) + renderScrollableContainer(containerId) + else if (useGrid) + renderResourceGrid() + else + renderResourceList(), + ], + ); + } +} diff --git a/lib/src/components/resource_link.dart b/lib/src/components/resource_link.dart new file mode 100644 index 0000000..055c78f --- /dev/null +++ b/lib/src/components/resource_link.dart @@ -0,0 +1,86 @@ +import 'package:jaspr/browser.dart'; + +enum CardVariant { + standard, + highlighted, + compact, + blueGlow, + greenGlow, + orangeGlow, +} + +class ResourceLink extends StatelessComponent { + const ResourceLink({ + super.key, + required this.title, + required this.description, + required this.url, + this.emoji, + this.variant = CardVariant.standard, + }); + + final String title; + final String description; + final String url; + final String? emoji; + final CardVariant variant; + + String get variantClassName { + switch (variant) { + case CardVariant.standard: + return ''; + case CardVariant.highlighted: + return 'highlighted'; + case CardVariant.compact: + return 'compact'; + case CardVariant.blueGlow: + return 'blue-glow'; + case CardVariant.greenGlow: + return 'green-glow'; + case CardVariant.orangeGlow: + return 'orange-glow'; + } + } + + @override + Iterable build(BuildContext context) sync* { + final variantClasses = ['resource-link']; + + if (variantClassName.isNotEmpty) { + variantClasses.add(variantClassName); + } + + yield a( + href: url, + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + classes: variantClasses.join(' '), + [ + if (emoji != null) + span( + classes: 'resource-emoji', + attributes: {'aria-hidden': 'true'}, + [text(emoji!)], + ), + div( + classes: 'resource-content', + [ + div( + classes: 'resource-title', + [ + if (emoji != null) text('$emoji '), + text(title), + ], + ), + div( + classes: 'resource-description', + [ + text(description), + ], + ), + ], + ), + ], + ); + } +} diff --git a/lib/src/components/survey_callout.dart b/lib/src/components/survey_callout.dart new file mode 100644 index 0000000..f51da62 --- /dev/null +++ b/lib/src/components/survey_callout.dart @@ -0,0 +1,76 @@ +import 'package:fc_ai_circle/src/components/take_survey.dart'; +import 'package:jaspr/browser.dart'; + +/// A callout section that prompts users to participate in the community survey. +/// +/// This component creates a visually distinct card with a title, description, +/// and a call-to-action button to take the survey. +class SurveyCallout extends StatelessComponent { + /// Creates a survey callout with customizable title and description. + const SurveyCallout({ + super.key, + this.title = 'Shape Our Community!', + this.description = + 'Be part of our growing community of Flutter developers exploring AI integration. Share your knowledge, learn from others, and build the future of AI-powered Flutter apps.', + this.emoji = 'πŸ””', + this.buttonText = 'Take the Survey', + this.isPulsing = true, + }); + + /// The headline text for the survey callout + final String title; + + /// The descriptive text explaining the survey's purpose + final String description; + + /// The emoji displayed beside the title + final String emoji; + + /// The text for the call-to-action button + final String buttonText; + + /// Whether the button should have a pulsing animation + final bool isPulsing; + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: 'survey-callout-container', + [ + div( + classes: 'survey-callout', + [ + // Title section with emoji + div( + classes: 'survey-header', + [ + span( + classes: 'emoji large-emoji', + attributes: {'aria-hidden': 'true'}, + [text(emoji)], + ), + h2(classes: 'survey-title', [text(title)]), + ], + ), + // Description + p( + classes: 'survey-description', + [text(description)], + ), + // Button container + div( + classes: 'buttons-container centered-buttons', + [ + // Use the enhanced TakeSurvey component directly + TakeSurvey( + buttonText: buttonText, + additionalClasses: isPulsing ? 'pulse-button' : '', + ), + ], + ), + ], + ), + ], + ); + } +} diff --git a/lib/src/components/take_survey.dart b/lib/src/components/take_survey.dart index 29821d1..3041895 100644 --- a/lib/src/components/take_survey.dart +++ b/lib/src/components/take_survey.dart @@ -1,16 +1,29 @@ import 'package:fc_ai_circle/src/app/external_links.dart'; import 'package:jaspr/browser.dart'; +/// A button component that links to the community survey. class TakeSurvey extends StatelessComponent { - const TakeSurvey({super.key}); + /// Creates a survey button with customizable text and additional classes. + const TakeSurvey({ + super.key, + this.buttonText = 'Take the Survey', + this.additionalClasses = '', + }); + + /// The text to display on the button + final String buttonText; + + /// Additional CSS classes to apply to the button + final String additionalClasses; @override Iterable build(BuildContext context) sync* { yield a( href: ExternalLink.surveyCommunity.url, target: Target.blank, - classes: 'cta_button', - [text('Take the Survey')], + attributes: {'rel': 'noopener noreferrer'}, + classes: 'cta_button ${additionalClasses.isNotEmpty ? additionalClasses : ''}', + [text(buttonText)], ); } } diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index 83005b6..12aefa9 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -1,17 +1,26 @@ +import 'package:fc_ai_circle/src/components/content_section.dart'; +import 'package:fc_ai_circle/src/components/feature_card.dart'; +import 'package:fc_ai_circle/src/components/page_hero.dart'; +import 'package:fc_ai_circle/src/components/resource_link.dart'; +import 'package:fc_ai_circle/src/components/resource_category.dart'; +import 'package:fc_ai_circle/src/components/survey_callout.dart'; import 'package:fc_ai_circle/src/layouts/page_layout.dart'; +import 'package:fc_ai_circle/src/app/external_links.dart'; import 'package:jaspr/browser.dart'; import 'package:jaspr_router/jaspr_router.dart'; +/// The Builders page showcases the community's purpose, mission and resources. class BuildersPage extends StatelessComponent { const BuildersPage({super.key}); - static var path = '/builders'; + static const path = '/builders'; - static Route route() { - return Route( + /// Defines the routing for this page + static Iterable route() sync* { + yield Route( path: path, - title: 'Builders', - builder: (BuildContext context, RouteState state) => BuildersPage(), + title: 'Builders - Flutter AI Builders Circle', + builder: (BuildContext context, RouteState state) => const BuildersPage(), ); } @@ -19,18 +28,302 @@ class BuildersPage extends StatelessComponent { Iterable build(BuildContext context) sync* { yield PageLayout( children: [ - h1([text('Builders')]), - h2([text('Our Mission 🎯')]), - p([text('We aim to:')]), - ul([ - li([ - text('Create accessible resources for Flutter developers ' - 'to incorporate AI into their apps') + _buildHeroSection(), + _buildAboutSection(), + _buildMissionSection(), + _buildWhatWeDoSection(), + _buildToolkitSection(), + const SurveyCallout(), // Using our new component + _buildContributorsSection(context), + ], + ); + } + + Component _buildHeroSection() { + return PageHero( + title: 'Builders', + description: 'An open-source, volunteer-led community focused on integrating AI capabilities ' + 'into Flutter applications.', + callout: EmojiCallout( + emoji: 'πŸš€', + children: [ + p([ + text( + 'Join us in creating agentic Flutter applications with advanced LLM integration, multimodal processing, and cutting-edge tools.') ]), - li([text('Explore and share best practices for responsible AI implementation')]), - li([text('Build a supportive community for experimentation and learning')]), - li([text('Develop open-source starter kits and reference implementations')]), - ]), + ], + ), + ); + } + + Component _buildAboutSection() { + return ContentSection( + title: 'About Us', + id: 'about', + emoji: '🌟', + description: 'We connect Flutter developers building with modern AI capabilities.', + children: [ + ContentCard( + elevated: true, + children: [ + p([ + text( + 'We bring together Flutter developers interested in building agentic apps using cutting-edge technologies like Gemini, Claude, OpenAI, Whisper, Multimodal Content Processing (MCP), and Retrieval-Augmented Generation (RAG).') + ]), + p([ + text( + 'Our community builds on the official Flutter ecosystem while exploring innovative ways to integrate AI into mobile and web applications.') + ]), + ], + ), + ], + ); + } + + Component _buildMissionSection() { + return ContentSection( + title: 'Our Mission', + id: 'mission', + emoji: '🎯', + children: [ + ResourceCategory( + title: 'Community Goals', + accent: CategoryAccent.purple, + useGrid: true, + resources: [ + ResourceLink( + title: 'Accessible Resources', + description: + 'Create clear documentation, tutorials, and examples for Flutter developers to incorporate AI into their apps', + url: ExternalLink.forumCategory.url, + ), + ResourceLink( + title: 'Best Practices', + description: + 'Explore and share responsible AI implementation patterns with the community', + url: ExternalLink.youTubeAgenticQA.url, + ), + ResourceLink( + title: 'Supportive Environment', + description: + 'Build a welcoming community for experimentation, learning, and collaboration', + url: ExternalLink.forumChat.url, + ), + ResourceLink( + title: 'Open Source', + description: 'Develop starter kits, reference implementations, and shared resources', + url: ExternalLink.socialGitHub.url, + ), + ], + ), + ], + ); + } + + Component _buildWhatWeDoSection() { + return ContentSection( + title: 'What We Do', + id: 'what-we-do', + emoji: 'πŸ“¦', + children: [ + div( + classes: 'features-grid', + attributes: {'role': 'list'}, + [ + FeatureCard( + title: 'Share Knowledge', + description: 'Technical articles, code examples, and implementation guides', + isHighlighted: true, + emoji: 'πŸ“š', + ), + FeatureCard( + title: 'Build Tools', + description: 'Open-source packages and utilities to simplify AI integration', + emoji: 'πŸ”§', + ), + FeatureCard( + title: 'Provide Templates', + description: 'Ready-to-use starter projects for common AI tasks', + isHighlighted: true, + emoji: 'πŸ“‹', + ), + FeatureCard( + title: 'Foster Collaboration', + description: 'Connect developers working on similar challenges', + emoji: '🀝', + ), + ], + ), + div(classes: 'section-spacer', []), + ContentSection( + title: 'YouTube Content', + emoji: 'πŸ’‘', + contentPadding: false, + children: [ + p([text('Interested in building with Flutter and AI? Check out our YouTube content:')]), + ResourceCategory( + useGrid: true, + accent: CategoryAccent.purple, + title: 'YouTube Resources', + resources: [ + ResourceLink( + title: 'AI Circle Playlist', + description: 'Collection of our community sessions and tutorials', + url: ExternalLink.youTubePlaylist.url, + ), + ResourceLink( + title: 'Vibe Coding Sessions', + description: 'Relaxed coding streams building AI-powered apps', + url: ExternalLink.youTubeVibeCoding.url, + ), + ResourceLink( + title: 'Agentic Q&A Session', + description: 'Deep dive into agentic app patterns', + url: ExternalLink.youTubeAgenticQA.url, + ), + ], + ), + ], + ), + ], + ); + } + + Component _buildToolkitSection() { + return ContentSection( + title: 'Builder\'s Toolkit', + id: 'toolkit', + emoji: '🧰', + description: 'Resources and tools to help you build agentic Flutter applications.', + children: [ + ResourceCategory( + title: 'Essential Resources', + accent: CategoryAccent.purple, + useGrid: true, + resources: [ + ResourceLink( + title: 'Function Calling with Gemini', + description: + 'Official documentation for implementing function calling with Gemini AI', + url: ExternalLink.geminiFunctionCallingDocs.url, + ), + ResourceLink( + title: 'MCP for Flutter Developers', + description: 'Understanding the Model Context Protocol for Flutter applications', + url: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, + ), + ], + ), + ResourceCategory( + title: 'Community Channels', + accent: CategoryAccent.green, + useGrid: true, + resources: [ + ResourceLink( + title: 'YouTube Channel', + description: 'Tutorials, livestreams, and coding sessions from our community', + url: ExternalLink.youTubeFlutterCommunity.url, + ), + ResourceLink( + title: 'Flutter Forum', + description: 'Discussions, Q&A, and knowledge sharing about Flutter and AI', + url: ExternalLink.forumCategory.url, + ), + ResourceLink( + title: 'Community Survey', + description: 'Help shape the future of our community by sharing your feedback', + url: ExternalLink.surveyCommunity.url, + ), + ], + ), + ], + ); + } + + Component _buildContributorsSection(BuildContext context) { + return ContentSection( + title: 'Want to Contribute?', + id: 'contributors', + emoji: 'πŸ”—', + elevated: true, + children: [ + ResourceCategory( + title: 'Ways to Contribute', + accent: CategoryAccent.blue, + useGrid: true, + resources: [ + ResourceLink( + title: 'Write Documentation', + description: 'Help create clear guides, examples, and tutorials for the community', + url: ExternalLink.socialGitHub.url, + ), + ResourceLink( + title: 'Create Example Apps', + description: 'Build sample projects demonstrating AI integration patterns', + url: ExternalLink.socialGitHub.url, + ), + ResourceLink( + title: 'Review Code', + description: 'Help ensure quality and consistency in our code samples', + url: ExternalLink.socialGitHub.url, + ), + ResourceLink( + title: 'Spread the Word', + description: 'Share our resources with your network and community', + url: ExternalLink.surveyCommunity.url, + ), + ], + ), + ContentCard( + elevated: true, + animateCta: true, + children: [ + div( + classes: 'volunteer-note', + [ + span(classes: 'emoji', attributes: {'aria-hidden': 'true'}, [text('βœ‹')]), + h3([text('Ready to contribute?')]), + p([ + text( + 'Take our contributor survey to let us know how you\'d like to participate. We\'re looking for writers, coders, reviewers, and community advocates.') + ]), + ], + ), + p([ + text('Check out our '), + a( + href: '/starters', + classes: 'highlight-link', + onClick: () => Router.of(context).push('/starters'), + [text('Starters')], + ), + text(' section for inspiration and examples to build upon.'), + ]), + div( + classes: 'buttons-container', + [ + a( + href: ExternalLink.surveyContributors.url, + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + classes: 'cta_button', + [ + span([text('Take Contributor Survey')]) + ], + ), + a( + href: ExternalLink.socialGitHub.url, + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + classes: 'secondary-button', + [ + span([text('GitHub Repository')]) + ], + ), + ], + ), + ], + ), ], ); } diff --git a/lib/src/pages/code_of_conduct_page.dart b/lib/src/pages/code_of_conduct_page.dart new file mode 100644 index 0000000..68a1a08 --- /dev/null +++ b/lib/src/pages/code_of_conduct_page.dart @@ -0,0 +1,129 @@ +import 'package:fc_ai_circle/src/components/content_section.dart'; +import 'package:fc_ai_circle/src/layouts/page_layout.dart'; +import 'package:jaspr/browser.dart'; +import 'package:jaspr_router/jaspr_router.dart'; + +/// Code of Conduct page for the Flutter Community AI Circle website. +class CodeOfConductPage extends StatelessComponent { + const CodeOfConductPage({super.key}); + + static const path = '/code-of-conduct'; + + /// Defines the routing for this page + static Iterable route() sync* { + yield Route( + path: path, + title: 'Code of Conduct - Flutter AI Builders Circle', + builder: (BuildContext context, RouteState state) => const CodeOfConductPage(), + ); + } + + @override + Iterable build(BuildContext context) sync* { + yield PageLayout( + children: [ + ContentSection( + title: 'Code of Conduct', + id: 'code-of-conduct', + emoji: '🀝', + children: [ + ContentCard( + elevated: true, + children: [ + h2([text('Flutter Community AI Circle Code of Conduct')]), + p([ + text('Last Updated: June 15, 2024'), + ]), + p([ + text( + 'The Flutter Community AI Circle is dedicated to providing a safe, inclusive, and welcoming environment for all community members, regardless of background, identity, or experience level.'), + ]), + h3([text('Our Pledge')]), + p([ + text( + 'In the interest of fostering an open and welcoming community, we pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.'), + ]), + h3([text('Expected Behavior')]), + p([ + text('As a member of this community, you are expected to:'), + ]), + ul([ + li([text('Be kind, respectful, and considerate to others')]), + li([text('Use welcoming and inclusive language')]), + li([text('Respect differing viewpoints and experiences')]), + li([text('Accept constructive criticism gracefully')]), + li([text('Focus on what is best for the community')]), + li([text('Show empathy towards other community members')]), + ]), + h3([text('Unacceptable Behavior')]), + p([ + text('The following behaviors are considered unacceptable:'), + ]), + ul([ + li([ + text('Harassment, intimidation, or discrimination in any form'), + ]), + li([ + text( + 'Offensive or derogatory comments related to personal characteristics or choices'), + ]), + li([ + text('Inappropriate or unwelcome attention or contact'), + ]), + li([ + text('Public or private harassment'), + ]), + li([ + text('Publishing others\' private information without explicit permission'), + ]), + li([ + text('Other conduct which could reasonably be considered inappropriate'), + ]), + ]), + h3([text('Enforcement')]), + p([ + text( + 'Violations of this Code of Conduct may result in temporary or permanent exclusion from community spaces or activities. The severity and details of enforcement will depend on the specific circumstances.'), + ]), + h3([text('Reporting')]), + p([ + text( + 'If you experience or witness behavior that violates this Code of Conduct, please report it by contacting the community organizers at '), + a( + href: 'mailto:flutter.community@example.com', + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + [text('flutter.community@example.com')], + ), + text('.'), + ]), + p([ + text( + 'All reports will be reviewed and investigated promptly and fairly. All community organizers are obligated to maintain confidentiality with regard to the reporter of an incident.'), + ]), + h3([text('Attribution')]), + p([ + text('This Code of Conduct is adapted from the '), + a( + href: 'https://www.contributor-covenant.org', + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + [text('Contributor Covenant')], + ), + text(', version 2.0, available at '), + a( + href: 'https://www.contributor-covenant.org/version/2/0/code_of_conduct.html', + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + [text('contributor-covenant.org/version/2/0/code_of_conduct.html')], + ), + text('.'), + ]), + ], + ), + ], + ), + ], + ); + } +} diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home_page.dart index b98b59e..e241169 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/pages/home_page.dart @@ -1,6 +1,7 @@ import 'package:fc_ai_circle/src/components/take_survey.dart'; import 'package:fc_ai_circle/src/layouts/page_layout.dart'; import 'package:fc_ai_circle/src/pages/builders_page.dart'; +import 'package:fc_ai_circle/src/app/external_links.dart'; import 'package:jaspr/browser.dart'; import 'package:jaspr_router/jaspr_router.dart'; @@ -42,7 +43,15 @@ class _HeroSection extends StatelessComponent { Iterable build(BuildContext context) sync* { yield section(classes: 'hero', [ div(classes: 'container', [ - h1([text('Build Agentic Flutter Experiences')]), + h1( + classes: 'page-title', + [ + span( + classes: 'title-text', + [text('Build Agentic Flutter Experiences')], + ), + ], + ), p([ text( 'A community-powered space for developers building ' @@ -58,7 +67,12 @@ class _HeroSection extends StatelessComponent { ), a( classes: 'secondary-button', - href: '#', + href: ExternalLink.youTubeAgenticQA.url, // The direct URL + target: Target.blank, // Jaspr's way to open in a new tab + attributes: { + 'rel': 'noopener noreferrer', + 'aria-label': 'Learn what Agentic Flutter is', + }, [text('What is Agentic Flutter?')], ), ]), @@ -80,21 +94,21 @@ class _FeaturesSection extends StatelessComponent { [text('Highlights from the Flutter Community AI Circle')], ), div(classes: 'features-grid', [ - // TODO: Add YouTube video embed for past livestream _FeatureCard( title: 'Past Livestream', - description: 'Vibe Coding a Card Game with Norbert & Friends', + description: 'FCAIC #1 - Prompt, Code, Think: Welcome to AI in Flutter', + url: + 'https://www.youtube.com/watch?v=Gub1DY8ScwU&list=PL4dBIh1xps-HIYvaEIbLWHZqt_WGBfpx3', ), - // TODO: Add calendar integration or dynamic content for upcoming events - // - can they add them to their calendar or directly go set a reminder? _FeatureCard( - title: 'Upcoming', - description: 'Humpday Q&A: Agentic Apps Spotlight', + title: 'Agentic Apps Spotlight', + description: 'Humpday Q&A: AI Special with Andrew Brogdon', + url: ExternalLink.youTubeAgenticQA.url, ), - // TODO: Add link to survey form/ or forms - maybe regular community one idk. _FeatureCard( title: 'Survey', description: 'Help shape open-source tooling for AI in Flutter', + url: ExternalLink.surveyCommunity.url, ), ]), ]), @@ -106,20 +120,37 @@ class _FeatureCard extends StatelessComponent { const _FeatureCard({ required this.title, required this.description, + this.url, }); final String title; final String description; + final String? url; @override Iterable build(BuildContext context) sync* { - yield div( - classes: 'feature-card', - [ - h3([text(title)]), - p([text(description)]), - ], - ); + final cardContent = [ + h3([text(title)]), + p([text(description)]), + ]; + + if (url != null) { + yield a( + classes: 'feature-card feature-card-link', + href: url!, + target: Target.blank, + attributes: { + 'rel': 'noopener noreferrer', + 'aria-label': 'Open $title resource', + }, + cardContent, + ); + } else { + yield div( + classes: 'feature-card', + cardContent, + ); + } } } diff --git a/lib/src/pages/privacy_policy_page.dart b/lib/src/pages/privacy_policy_page.dart new file mode 100644 index 0000000..aab4959 --- /dev/null +++ b/lib/src/pages/privacy_policy_page.dart @@ -0,0 +1,114 @@ +import 'package:fc_ai_circle/src/components/content_section.dart'; +import 'package:fc_ai_circle/src/layouts/page_layout.dart'; +import 'package:jaspr/browser.dart'; +import 'package:jaspr_router/jaspr_router.dart'; + +/// Privacy Policy page for the Flutter Community AI Circle website. +class PrivacyPolicyPage extends StatelessComponent { + const PrivacyPolicyPage({super.key}); + + static const path = '/privacy-policy'; + + /// Defines the routing for this page + static Iterable route() sync* { + yield Route( + path: path, + title: 'Privacy Policy - Flutter AI Builders Circle', + builder: (BuildContext context, RouteState state) => const PrivacyPolicyPage(), + ); + } + + @override + Iterable build(BuildContext context) sync* { + yield PageLayout( + children: [ + ContentSection( + title: 'Privacy Policy', + id: 'privacy-policy', + emoji: 'πŸ”’', + children: [ + ContentCard( + elevated: true, + children: [ + h2([text('Privacy Policy for Flutter Community AI Circle')]), + p([ + text('Last Updated: June 15, 2024'), + ]), + p([ + text( + 'The Flutter Community AI Circle values your privacy and is committed to protecting your personal data. This Privacy Policy explains how we collect, use, and safeguard your information when you visit our website or participate in our community.'), + ]), + h3([text('Information We Collect')]), + p([ + text('We may collect and process the following types of personal data:'), + ]), + ul([ + li([ + text( + 'Information you provide when completing surveys or forms (name, email, etc.)'), + ]), + li([ + text( + 'Usage data including how you interact with our website and community resources'), + ]), + li([ + text( + 'Information from third-party services when you engage with our community through those platforms (GitHub, Discord, etc.)'), + ]), + ]), + h3([text('How We Use Your Information')]), + p([ + text('We use the information we collect to:'), + ]), + ul([ + li([text('Improve our resources and community offerings')]), + li([text('Communicate with you about events, updates, and opportunities')]), + li([text('Customize content and experiences based on your interests')]), + li([text('Analyze and understand how our resources are being used')]), + ]), + h3([text('Data Security')]), + p([ + text( + 'We implement appropriate technical and organizational measures to protect your personal data. However, no method of transmission over the Internet is 100% secure.'), + ]), + h3([text('Third-Party Services')]), + p([ + text( + 'Our website and community resources may contain links to third-party websites and services. We are not responsible for the privacy practices of these external sites.'), + ]), + h3([text('Your Rights')]), + p([ + text( + 'Depending on your location, you may have rights regarding your personal data, including:'), + ]), + ul([ + li([text('Access to your personal data')]), + li([text('Correction of inaccurate data')]), + li([text('Deletion of your data')]), + li([text('Restriction of processing')]), + li([text('Data portability')]), + ]), + h3([text('Updates to This Policy')]), + p([ + text( + 'We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new policy on this page.'), + ]), + h3([text('Contact Us')]), + p([ + text('If you have questions about this Privacy Policy, please contact us at '), + a( + href: 'mailto:flutter.community@example.com', + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + [text('flutter.community@example.com')], + ), + text('.'), + ]), + ], + ), + ], + ), + ], + ); + } +} diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index c2cd040..b218c31 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -1,16 +1,21 @@ +import 'package:fc_ai_circle/src/components/content_section.dart'; +import 'package:fc_ai_circle/src/components/page_hero.dart'; +import 'package:fc_ai_circle/src/components/resource_link.dart'; +import 'package:fc_ai_circle/src/components/resource_category.dart'; import 'package:fc_ai_circle/src/layouts/page_layout.dart'; +import 'package:fc_ai_circle/src/app/external_links.dart'; import 'package:jaspr/browser.dart'; import 'package:jaspr_router/jaspr_router.dart'; class StartersPage extends StatelessComponent { const StartersPage({super.key}); - static var path = '/starters'; + static const path = '/starters'; - static Route route() { - return Route( + static Iterable route() sync* { + yield Route( path: path, - title: 'Starters', + title: 'Starters - Flutter AI Builders Circle', builder: (BuildContext context, RouteState state) => StartersPage(), ); } @@ -19,7 +24,396 @@ class StartersPage extends StatelessComponent { Iterable build(BuildContext context) sync* { yield PageLayout( children: [ - h1([text('Starters')]), + _buildHeroSection(), + _buildCoreConceptsSection(), + _buildOfficialDocsSection(), + _buildDeepenUnderstandingSection(), + _buildContributeSection(), + _buildComingNextSection(), + ], + ); + } + + Component _buildHeroSection() { + return PageHero( + title: 'Starters', + description: 'Welcome to the Starters Hub of the Flutter AI Builders Circle. ' + 'This section offers high-quality, community-vetted resources to help you ' + 'explore and build agentic apps using Flutter and AI β€” from basic LLM ' + 'integrations to more advanced workflows using RAG, MCP, and Whisper.', + callout: EmojiCallout( + emoji: '🧠', + children: [ + p([ + text( + 'We believe in learning by doing β€” and starting with official sources. ' + 'All links below come from official documentation or deeply trusted platforms.', + ), + ]), + ], + ), + ); + } + + Component _buildCoreConceptsSection() { + return ContentSection( + title: 'Core Concepts You Should Know', + id: 'core-concepts', + emoji: '🧩', + children: [ + ResourceCategory( + title: 'Key Concepts', + useGrid: true, + resources: [ + ResourceLink( + title: 'LangChain: Introduction', + description: + 'Learn the foundations of LangChain and its modular architecture for building LLM-powered agents.', + url: 'https://python.langchain.com/docs/introduction/', + ), + ResourceLink( + title: 'What is Agentic AI? (IBM)', + description: + 'Overview of how agentic AI differs from traditional AI, with real-world examples.', + url: + 'https://www.ibm.com/think/topics/agentic-ai#:~:text=Agentic%20AI%20solutions%20can%20be,predictive%20analytics%20and%20execute%20trades.', + ), + ResourceLink( + title: 'Introducing the Model Context Protocol (Anthropic)', + description: 'Learn how MCP standardizes interactions between LLMs and tools.', + url: 'https://www.anthropic.com/news/model-context-protocol', + ), + ResourceLink( + title: 'Amazon AWS: What is RAG (Retrieval-Augmented Generation)?', + description: + 'A simple primer on Retrieval-Augmented Generation and how it improves model accuracy.', + url: 'https://aws.amazon.com/what-is/retrieval-augmented-generation/', + ), + ResourceLink( + title: 'Observable Flutter #59 – Agentic Apps with Flutter', + description: + 'Craig Labenz and Andrew Brogdon walk through agentic patterns, Colorist, and Firebase Genkit.', + url: + 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', + ), + ], + ), + ], + ); + } + + Component _buildOfficialDocsSection() { + return ContentSection( + title: 'Official Docs & Tools Worth Exploring', + id: 'official-docs', + emoji: 'πŸ”—', + children: [ + ResourceCategory( + title: 'LLM Integration (Gemini, OpenAI, Claude)', + accent: CategoryAccent.purple, + resources: [ + ResourceLink( + title: 'Gemini API + Flutter (Colorist Codelab)', + description: + 'Beginner/Intermediate. Learn how to integrate Gemini into a Flutter app using Firebase extensions and function calling.', + url: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', + ), + ResourceLink( + title: 'OpenAI API Docs', + description: + 'Beginner. Covers completions, chat, and function calling for GPT models.', + url: 'https://platform.openai.com/docs', + ), + ResourceLink( + title: 'Anthropic Claude API', + description: + 'Beginner. Official Claude API docs with example request/response flows.', + url: 'https://docs.anthropic.com/claude/docs', + ), + ResourceLink( + title: 'mcp_dart (Community SDK)', + description: + 'Integrate LLM applications with external data and tools using this Dart SDK for the Model Context Protocol (MCP).', + url: 'https://pub.dev/packages/mcp_dart', + ), + ResourceLink( + title: 'dart_openai (Community SDK)', + description: + 'Intermediate. A Dart package that simplifies using OpenAI APIs within Flutter projects.', + url: 'https://pub.dev/packages/dart_openai', + ), + ], + ), + ResourceCategory( + title: 'Speech-to-Text (Whisper)', + accent: CategoryAccent.green, + resources: [ + ResourceLink( + title: 'OpenAI Whisper API', + description: + 'Beginner. Transcribe audio to text using OpenAI\'s hosted Whisper model.', + url: 'https://platform.openai.com/docs/guides/speech-to-text', + ), + ResourceLink( + title: 'flutter_whisper.cpp', + description: + 'Advanced. Community-maintained Flutter app running Whisper on-device via Rust FFI and whisper.cpp.', + url: 'https://github.com/lyledean1/flutter_whisper.cpp', + ), + ], + ), + ResourceCategory( + title: 'Firebase Vector Search & RAG', + accent: CategoryAccent.orange, + useGrid: true, + resources: [ + ResourceLink( + title: 'Firestore Vector Search (Extension)', + description: + 'Intermediate. Store and retrieve vector embeddings in Firestore for building RAG pipelines.', + url: 'https://extensions.dev/extensions/googlecloud/firestore-vector-search', + ), + ResourceLink( + title: 'Firebase Genkit', + description: + 'Advanced. A backend framework for building AI-driven workflows with Gemini, designed for Cloud Functions.', + url: 'https://firebase.google.com/docs/genkit', + ), + ], + ), + ResourceCategory( + title: 'MCP (Model Context Protocol)', + accent: CategoryAccent.blue, + resources: [ + ResourceLink( + title: 'Model Context Protocol Overview', + description: + 'Advanced. MCP is an open protocol to connect LLMs with tools and data providers.', + url: 'https://modelcontextprotocol.io/introduction', + ), + ], + ), + ResourceCategory( + title: 'Local LLMs (Ollama)', + resources: [ + ResourceLink( + title: 'Ollama', + description: + 'Intermediate. Easily run LLMs like LLaMA or Mistral locally and serve them via HTTP.', + url: 'https://github.com/ollama/ollama', + ), + ], + ), + ResourceCategory( + title: 'Agentic Patterns', + accent: CategoryAccent.purple, + useGrid: true, + resources: [ + ResourceLink( + title: 'Function Calling (OpenAI)', + description: + 'Intermediate. Enables models to call structured functions from your app.', + url: 'https://platform.openai.com/docs/guides/function-calling', + ), + ResourceLink( + title: 'LLM Agents (Prompt Engineering Guide)', + description: 'Advanced. Explore ReAct and other reasoning/action agent patterns.', + url: 'https://www.promptingguide.ai/techniques/react', + ), + ], + ), + ResourceCategory( + title: 'Codelabs, Templates, and Examples', + accent: CategoryAccent.green, + useGrid: true, + resources: [ + ResourceLink( + title: 'Colorist (Gemini + Flutter)', + description: + 'Intermediate. Gemini-powered Flutter app that updates the UI based on natural language prompts.', + url: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', + ), + ResourceLink( + title: 'Gemini API Cookbook (GitHub)', + description: + 'Intermediate/Advanced. Google\'s official examples for calling Gemini API across languages including Dart.', + url: 'https://github.com/google-gemini/cookbook', + ), + ResourceLink( + title: 'Gemini Flutter Weather App', + description: + 'Beginner/Intermediate. Weather app powered by Gemini AI with natural language interaction.', + url: 'https://github.com/flutter/samples/tree/main/ai_weather', + ), + ResourceLink( + title: 'OpenAI Translator Example', + description: + 'Intermediate. Demonstrates using OpenAI to build a multilingual translator app.', + url: + 'https://github.com/openai/openai-cookbook/blob/main/examples/How_to_build_a_multilingual_translator.md', + ), + ], + ), + ResourceCategory( + title: 'Dart AI Libraries', + resources: [ + ResourceLink( + title: 'LangChain.dart', + description: + 'Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more.', + url: 'https://pub.dev/packages/langchain', + ), + ResourceLink( + title: 'Dart OpenAI', + description: + 'Beginner/Intermediate. A Dart client for OpenAI API with chat, completions, embeddings and more.', + url: 'https://pub.dev/packages/dart_openai', + ), + ], + ), + ], + ); + } + + Component _buildDeepenUnderstandingSection() { + return ContentSection( + title: 'Want to Deepen Your Understanding?', + id: 'deepen-understanding', + emoji: '🧠', + children: [ + ResourceCategory( + title: 'Additional Resources', + useGrid: true, + resources: [ + ResourceLink( + title: 'Agentic Apps with Flutter (Observable Flutter #59)', + description: + 'Craig Labenz and Andrew Brogdon discuss and explore AI-powered, agentic apps in Flutter in this Observable Flutter episode.', + url: + 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', + ), + ResourceLink( + title: 'Gemini API: Function Calling Docs', + description: + 'Official Gemini API docs on connecting models to external tools and APIs. Learn how function calling bridges natural language to real-world actions and data.', + url: ExternalLink.geminiFunctionCallingDocs.url, + ), + ResourceLink( + title: 'Jhin Lee on mcp_dart (Humpday Q&A)', + description: + 'GDE Jhin Lee discusses the mcp_dart package and the Model Context Protocol. (MCP segment starts at 29:54).', + url: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, + ), + ], + ), + ], + ); + } + + Component _buildContributeSection() { + return ContentSection( + title: 'Contribute or Join the Circle', + id: 'contribute', + emoji: '🀝', + elevated: true, + children: [ + ContentCard( + elevated: true, + animateCta: true, + children: [ + p([ + text( + 'We\'re building these kits and docs as a community. If you\'d like to:', + ) + ]), + ul( + attributes: {'role': 'list'}, + [ + li([text('Submit your own starter')]), + li([text('Add a resource to this list')]), + li([text('Help write docs or translate examples')]), + ], + ), + p([ + span( + classes: 'cta_button', + [ + a( + href: ExternalLink.surveyContributors.url, + target: Target.blank, + attributes: { + 'aria-label': 'Contribute: Take the Contributors Survey', + 'rel': 'noopener noreferrer', + }, + [text('Become a Contributor')], + ) + ], + ) + ]) + ], + ), + ], + ); + } + + Component _buildComingNextSection() { + return ContentSection( + title: 'Stay Connected & See What\'s Next', + id: 'coming-next', + emoji: 'πŸ“Ί', + elevated: true, + children: [ + ContentCard( + elevated: true, + children: [ + h3([text('Catch Our Streams & Join the Conversation!')]), + p([ + text( + 'Tune into live Humpday Q&As, Vibe Coding sessions, and exclusive AI Circle content on the official Flutter Community channel. Then, chat with us on the forum!') + ]), + div(classes: 'buttons-container', [ + a( + classes: 'cta_button', + href: ExternalLink.youTubeFlutterCommunity.url, + target: Target.blank, + attributes: { + 'aria-label': 'Watch Flutter Community on YouTube', + 'rel': 'noopener noreferrer' + }, + [ + text('▢️ Watch on YouTube') + ]), + a( + classes: 'secondary-button', + href: ExternalLink.forumChat.url, + target: Target.blank, + attributes: { + 'aria-label': 'Join the Flutter Community Forum', + 'rel': 'noopener noreferrer' + }, + [ + text('πŸ’¬ Join the Forum') + ]), + ]), + ], + ), + // Simple, centered volunteer note (no longer in a card) + p( + styles: Styles( + textAlign: TextAlign.center, + raw: { + 'color': 'var(--secondary-text)', + 'margin-top': 'var(--spacing-lg)', + }, + fontSize: Unit.em(0.9), // Proper Unit for fontSize + ), + [ + span(classes: 'emoji', [text('🌱')]), + text(' This space is volunteer-built with '), + span(classes: 'emoji', [text('❀️')]), + text(' β€” we aim to make agentic Flutter development more accessible for everyone.'), + ], + ), ], ); } diff --git a/pubspec.lock b/pubspec.lock index d4acbe0..87f0c5f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -618,4 +618,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.6.2 <3.8.0-z" + dart: ">=3.6.0 <3.8.0-z" diff --git a/pubspec.yaml b/pubspec.yaml index 701c257..3d79b8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/fluttercommunity/ai_builders_circle version: 1.0.0 environment: - sdk: ^3.6.2 + sdk: '>=3.3.0 <4.0.0' dependencies: jaspr: ^0.18.0 diff --git a/web/css/clickable-cards.css b/web/css/clickable-cards.css new file mode 100644 index 0000000..a810ed9 --- /dev/null +++ b/web/css/clickable-cards.css @@ -0,0 +1,136 @@ +/* Make resource cards fully clickable */ +/* The .resource-link class is now on an tag */ +a.resource-link { + display: flex; + flex-direction: row; + align-items: center; + padding: 1rem; + text-decoration: none; + color: inherit; + position: relative; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +a.resource-link:hover { + transform: translateY(-2px); +} + +/* Style for the title, which is now a span */ +.resource-link .resource-title { + font-weight: bold; + margin-bottom: 0.25em; +} + +/* Adjust emoji and content div styling if needed due to flex layout on a.resource-link */ +.resource-link .resource-emoji { + margin-right: 0.75em; + font-size: 1.5em; +} + +.resource-link .resource-content { + display: flex; + flex-direction: column; + flex: 1; +} + +.resource-link .resource-description { + font-size: 0.9em; + color: var(--text-secondary-color, #555); + line-height: 1.4; +} + +/* Remove the clickable overlay - JavaScript will handle this +.resource-link::before { + content: ''; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1; +} +*/ + +.resource-link a.resource-title { + position: relative; + /* z-index: 2; */ /* No longer needed as the overlay is removed */ +} + +/* Survey callout styling */ +.survey-callout-container { + display: flex; /* Optional: for centering the callout if it has a max-width */ + justify-content: center; /* Optional: for centering */ + padding: 2rem 0; /* Add some vertical spacing around the callout */ +} + +.survey-callout { + background-color: var(--primary-color, #2c3e50); /* Use a theme variable or a dark bg */ + color: var(--text-color-light, #ecf0f1); /* Light text for dark background */ + padding: 2rem 2.5rem; /* Generous padding */ + border-radius: 12px; /* Soft rounded corners */ + /* box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); */ /* Softer, more modern shadow if needed, or remove if it was causing issues */ + /* Remove overflow: hidden unless specifically needed for a child element */ + /* max-width: 800px; */ /* Optional: constrain width */ + /* margin: 3rem auto; */ /* Replaced by survey-callout-container for centering */ +} + +.survey-header { + display: flex; + align-items: center; + margin-bottom: 1.25rem; /* Increased spacing */ + gap: 1rem; +} + +.large-emoji { + font-size: 2.8rem; /* Slightly larger emoji */ + /* padding: 0.5rem; */ /* Padding might not be needed if alignment is good */ + display: inline-block; +} + +.survey-title { + margin: 0; + font-size: 1.6rem; /* Adjusted for new shorter title */ + font-weight: 600; /* Bolder title */ + line-height: 1.3; + color: inherit; /* Inherit from .survey-callout */ +} + +.survey-description { + font-size: 1rem; /* Standard paragraph size */ + line-height: 1.6; + margin-bottom: 1.75rem; /* More space before the button */ + color: inherit; /* Inherit from .survey-callout, or slightly less prominent */ + opacity: 0.9; /* Slightly less prominent description text */ +} + +.centered-buttons { + display: flex; + justify-content: center; + margin-top: 1.5rem; +} + +.pulse-button { + animation: pulse 2s infinite; +} + +.pulse-button-wrapper a { + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4); + } + 70% { + box-shadow: 0 0 0 10px rgba(255, 255, 255, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); + } +} + +/* Utility class for vertical spacing between sections */ +.section-spacer { + height: 2em; + width: 100%; +} diff --git a/web/css/styles.css b/web/css/styles.css index 59a4005..b7bef86 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -1,11 +1,12 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); -html, body { +html, +body { width: 100%; height: 100%; margin: 0; padding: 0; - font-family: 'Roboto', sans-serif; + font-family: "Roboto", sans-serif; background-color: #000; color: #fff; display: flex; @@ -33,10 +34,13 @@ body > * { --spacing-xl: 60px; --transition-speed: 0.3s; --navbar-height: 120px; + --border-color: rgba(255, 255, 255, 0.1); + --heading-color: var(--accent-color); + --bg-color: rgba(0, 0, 0, 0.8); } body { - font-family: 'Inter', sans-serif; + font-family: "Inter", sans-serif; background-color: var(--background); color: var(--text-color); line-height: 1.5; @@ -77,7 +81,9 @@ a:hover { .cta_button:hover { background: var(--accent-gradient); color: #fff; - box-shadow: 0 0 12px var(--shadow-color), 0 0 2px var(--accent-color); + box-shadow: + 0 0 12px var(--shadow-color), + 0 0 2px var(--accent-color); transform: translateY(-2px) scale(1.03); filter: brightness(1.1); } @@ -92,6 +98,7 @@ a:hover { color: var(--text-color); font-weight: 600; transition: all var(--transition-speed); + cursor: pointer; } .secondary-button:hover { @@ -101,9 +108,7 @@ a:hover { transform: translateY(-2px); } - .page-layout { - width: 100%; margin-top: var(--navbar-height); } @@ -114,17 +119,44 @@ a:hover { display: flex; flex-direction: column; justify-content: center; - background: radial-gradient(circle at 50% 50%, var(--primary-color), var(--background)); + background: var(--background); position: relative; overflow: hidden; } +/* Replace the complex animated gradient with a simple, static subtle spotlight */ +/* +.hero:before { + content: ''; + position: absolute; + top: -20%; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient( + circle at 50% 30%, + rgba(171, 87, 255, 0.03) 0%, + transparent 70% + ); + /* Remove animation that causes visual noise */ +/* animation: pulse-bg 10s infinite alternate ease-in-out; */ +/* +} +*/ + +/* Remove the distracting animation */ +/* @keyframes pulse-bg { + 0% { opacity: 0.5; transform: scale(1); } + 100% { opacity: 0.8; transform: scale(1.1); } +} */ + .hero .container { text-align: center; max-width: 800px; margin: 0 auto; } +/* Improve title treatment */ .hero h1 { font-size: 64px; margin-bottom: var(--spacing-md); @@ -140,13 +172,17 @@ a:hover { color: var(--secondary-text); } -.hero .buttons-container { +/* General utility for a container of buttons, typically centered */ +.buttons-container { display: flex; + flex-wrap: wrap; /* Allow buttons to wrap on smaller screens if needed */ gap: var(--spacing-md); justify-content: center; + margin-top: var(--spacing-lg); /* Consistent spacing from content above */ } .hero .button { + /* This rule seems to style text within a .button class, not the container */ font-size: 18px; } @@ -273,11 +309,49 @@ footer a { transition: color var(--transition-speed); } -footer a:hover, a.selected { +footer a:hover, +a.selected { color: var(--accent-color); } -/* Navbar styles */ +/* Styles for 'Coming soon' links in the footer */ +.footer-link.coming-soon-link { + color: var(--secondary-text); /* Match disabled/inactive link appearance */ + cursor: help; /* Indicate that it provides information but isn't a direct link */ + position: relative; /* For tooltip positioning */ + text-decoration: none; /* Ensure no underline like active links */ +} + +.footer-link.coming-soon-link:hover { + color: var(--secondary-text); /* Keep color same on hover, no accent */ +} + +.footer-link.coming-soon-link[data-tooltip]:hover::after { + content: attr(data-tooltip); /* Display the tooltip text */ + position: absolute; + bottom: 100%; /* Position above the text */ + left: 50%; + transform: translateX(-50%); /* Center the tooltip */ + margin-bottom: 5px; /* Space between text and tooltip */ + padding: 6px 10px; + background-color: var(--primary-color); /* Dark background for tooltip */ + color: var(--text-color); + border: 1px solid var(--accent-color); + border-radius: var(--border-radius); + font-size: 0.85em; + white-space: nowrap; /* Prevent tooltip text from wrapping */ + z-index: 10; /* Ensure tooltip is above other elements */ + opacity: 0; /* Start hidden */ + animation: fadeInTooltip 0.3s forwards; +} + +@keyframes fadeInTooltip { + to { + opacity: 1; + } +} + +/* Updated Navbar styles */ .navbar { position: fixed; top: 0; @@ -285,34 +359,65 @@ footer a:hover, a.selected { right: 0; z-index: 100; padding: 20px 0; - background-color: rgba(0, 0, 0, 0.8); + background-color: var(--bg-color); backdrop-filter: blur(10px); + border-bottom: 1px solid var(--border-color); } .navbar .container { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--spacing-md); +} + +.navbar-top-row { display: flex; justify-content: space-between; align-items: center; + width: 100%; } .navbar-brand { font-size: 24px; font-weight: bold; color: var(--accent-color); + display: flex; + align-items: center; +} + +.navbar-brand img { + height: 50px; +} + +.nav-menu-container { + display: flex; + width: 100%; + justify-content: space-between; + align-items: center; } .nav-links { display: flex; gap: 30px; + flex-wrap: wrap; + align-items: center; } .nav-link { + color: var(--text-color); + text-decoration: none; font-weight: 500; position: relative; + padding: 0.5rem 0; + font-size: 1rem; } .nav-link::after { - content: ''; + content: ""; position: absolute; width: 0; height: 2px; @@ -322,14 +427,247 @@ footer a:hover, a.selected { transition: width var(--transition-speed); } -.nav-link:hover::after { +.nav-link:hover::after, +.nav-link.nav-selected::after { width: 100%; } -.nav-selected { +.nav-link:hover, +.nav-link.nav-selected { color: var(--accent-color); } +.navbar-cta { + margin-left: auto; + padding-left: 20px; +} + +.mobile-menu-toggle { + display: none; + flex-direction: column; + justify-content: space-between; + width: 30px; + height: 21px; + cursor: pointer; + z-index: 101; +} + +.mobile-menu-toggle span { + display: block; + height: 3px; + background-color: var(--text-color); + border-radius: 3px; + transition: all 0.3s ease; +} + +/* Resource cards styles */ +.resource-link { + display: flex; + gap: var(--spacing-md); + padding: var(--spacing-md); + background-color: var(--card-bg); + border-radius: var(--border-radius); + margin-bottom: var(--spacing-md); + transition: all var(--transition-speed); + border: 1px solid var(--border-color); + height: 100%; + box-sizing: border-box; + text-decoration: none; + color: var(--text-color); +} + +.resource-link:hover { + transform: translateY(-2px); + box-shadow: 0 0 15px rgba(171, 87, 255, 0.15); + border-color: rgba(171, 87, 255, 0.2); +} + +.resource-emoji { + font-size: 28px; + display: flex; + align-items: center; + justify-content: center; + min-width: 36px; + background: rgba(171, 87, 255, 0.1); + height: 48px; + width: 48px; + border-radius: 12px; +} + +.resource-content { + flex: 1; +} + +.resource-title { + font-weight: 600; + color: var(--heading-color); + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 0.5rem; +} + +.resource-description { + color: var(--secondary-text); + margin: 0; + font-size: 14px; + line-height: 1.6; +} + +/* Card variants - restore styling */ +.resource-link.highlighted { + border: 1px solid rgba(171, 87, 255, 0.15); + background-color: rgba(171, 87, 255, 0.02); +} + +.resource-link.highlighted:hover { + box-shadow: 0 0 15px rgba(171, 87, 255, 0.2); + border-color: rgba(171, 87, 255, 0.25); +} + +.resource-link.blue-glow { + border: 1px solid rgba(78, 159, 255, 0.15); + background-color: rgba(78, 159, 255, 0.02); +} + +.resource-link.blue-glow:hover { + box-shadow: 0 0 15px rgba(78, 159, 255, 0.2); + border-color: rgba(78, 159, 255, 0.25); +} + +.resource-link.green-glow { + border: 1px solid rgba(87, 255, 171, 0.15); + background-color: rgba(87, 255, 171, 0.02); +} + +.resource-link.green-glow:hover { + box-shadow: 0 0 15px rgba(87, 255, 171, 0.2); + border-color: rgba(87, 255, 171, 0.25); +} + +.resource-link.orange-glow { + border: 1px solid rgba(255, 159, 78, 0.15); + background-color: rgba(255, 159, 78, 0.02); +} + +.resource-link.orange-glow:hover { + box-shadow: 0 0 15px rgba(255, 159, 78, 0.2); + border-color: rgba(255, 159, 78, 0.25); +} + +/* Mobile responsive navbar */ +@media (max-width: 768px) { + .navbar .container { + flex-wrap: wrap; + padding: 10px var(--spacing-sm); + } + + .mobile-menu-toggle { + display: flex; + } + + .nav-menu-container { + flex-direction: column; + align-items: flex-start; + display: none; + width: 100%; + } + + .nav-menu-container.open { + display: flex; + } + + .nav-links { + flex-direction: column; + width: 100%; + margin: 1rem 0; + gap: 1rem; + } + + .nav-link { + padding: 0.7rem 0; + width: 100%; + display: block; + font-size: 1.1rem; + } + + .navbar-cta { + width: 100%; + margin: 1rem 0 0 0; + padding: 0; + } + + .navbar-cta a { + text-align: center; + display: block; + } + + /* Adjust page layout below the navbar */ + .page-layout { + margin-top: calc(var(--navbar-height) + 60px); + } + + /* Ensure the navbar can expand vertically */ + .navbar { + height: auto; + min-height: var(--navbar-height); + } + + /* Ensure content properly starts below navbar */ + .hero { + padding-top: 40px; + } + + /* Ensure font sizes are readable on mobile */ + body { + font-size: 16px; + } + + /* Enlarge touch targets for better mobile interaction */ + .nav-link, + .footer a, + .resource-link { + padding: 10px 0; + } + + /* Add extra space around interactive elements */ + .cta_button, + .secondary-button, + .nav-link { + min-height: 44px; /* Minimum Apple recommended touch target size */ + display: flex; + align-items: center; + justify-content: center; + } + + /* Ensure images are responsive */ + img { + max-width: 100%; + height: auto; + } + + /* Resource grid for mobile */ + .resource-grid { + grid-template-columns: 1fr; + padding: 0 1rem; + } + + /* Improve resource link spacing on mobile */ + .resource-link { + padding: var(--spacing-md); + display: flex; + flex-direction: row; + } + + /* Make emoji smaller on mobile */ + .resource-emoji { + font-size: 24px; + min-width: 24px; + height: 36px; + width: 36px; + } +} + /* FAQ Section */ #faq { padding: var(--spacing-xl) 0; @@ -370,12 +708,1056 @@ footer a:hover, a.selected { /* Animation Hints - for future implementation */ .section-title { - /* - TODO: Implement scroll-based animations - Add fade-in and slide-up animation when section comes into viewport - Example: - .section-title.visible { - animation: fadeSlideUp 0.8s ease forwards; - } - */ + font-size: 42px; + margin-bottom: var(--spacing-xl); + /* Remove any animated or decorative elements */ +} + +/* Category sections */ +.category-section { + margin-bottom: var(--spacing-xl); +} + +.category-section h3 { + font-size: 28px; + margin-bottom: var(--spacing-md); + color: var(--accent-color); +} + +.category-section ul { + list-style: none; + padding: 0; + margin: 0; +} + +.category-section li { + margin-bottom: var(--spacing-md); + padding: var(--spacing-md); + background-color: var(--card-bg); + border-radius: var(--border-radius); + transition: all var(--transition-speed); +} + +.category-section li:hover { + transform: translateX(10px); + box-shadow: 0 0 20px var(--shadow-color); +} + +.category-section a { + color: var(--accent-color); + font-weight: 600; +} + +.category-section a:hover { + color: var(--text-color); +} + +/* Blockquote styles */ +blockquote { + margin: var(--spacing-lg) 0; + padding: var(--spacing-md); + border-left: 4px solid var(--accent-color); + background-color: var(--card-bg); + border-radius: 0 var(--border-radius) var(--border-radius) 0; +} + +blockquote p { + margin: 0; + font-style: italic; + color: var(--secondary-text); +} + +/* Section spacing */ +section { + padding: var(--spacing-xl) 0; +} + +section:first-child { + padding-top: 0; +} + +section:last-child { + padding-bottom: 0; +} + +/* Focus styles for better keyboard navigation */ +a:focus-visible, +button:focus-visible { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-radius: var(--border-radius); +} + +/* Improve focus visibility for interactive elements */ +.feature-card:focus-within, +.category-section li:focus-within { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-radius: var(--border-radius); +} + +/* Ensure focus styles are visible in high contrast mode */ +@media (forced-colors: active) { + a:focus-visible, + button:focus-visible, + .feature-card:focus-within, + .category-section li:focus-within { + outline: 2px solid CanvasText; + } +} + +/* Page title styling */ +.page-title { + font-size: 64px; + text-align: center; + margin-bottom: var(--spacing-md); +} + +.page-title .title-text { + background: var(--accent-gradient); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + position: relative; + display: inline-block; + font-size: clamp(48px, 6vw, 72px); + /* Remove text-shadow to improve clarity */ + /* text-shadow: 0 0 20px rgba(171, 87, 255, 0.3); */ +} + +/* Remove the gradient underline effect */ +/* +.page-title .title-text::after { + content: ''; + position: absolute; + bottom: -8px; + left: 0; + width: 100%; + height: 2px; + background: var(--accent-gradient); + transform: scaleX(0.8); + opacity: 0.5; + border-radius: 2px; +} +*/ + +/* Linear.app style content section */ +.content-section { + padding: 64px 0; + position: relative; +} + +/* Remove the divider +.content-section:not(:last-child):after { + content: ''; + height: 4px; + width: 40px; + background: var(--accent-gradient); + opacity: 0.2; + border-radius: 2px; + margin: 32px auto 0; + display: block; +} +*/ + +.section-content { + padding: var(--spacing-md) 0; +} + +.section-description { + font-size: 18px; + max-width: 800px; + color: var(--secondary-text); + margin-bottom: var(--spacing-md); +} + +/* Cards */ +.content-card { + background-color: var(--card-bg); + border-radius: 8px; + padding: var(--spacing-md); + margin-bottom: var(--spacing-md); + border: 1px solid rgba(255, 255, 255, 0.05); + transition: all var(--transition-speed); +} + +.content-card.elevated { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.content-card:hover { + transform: translateY(-2px); + border-color: rgba(171, 87, 255, 0.2); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +/* Resource links */ +.resource-link { + display: flex; + gap: var(--spacing-md); + padding: var(--spacing-md); + background-color: var(--card-bg); + border-radius: var(--border-radius); + margin-bottom: var(--spacing-md); + transition: all var(--transition-speed); + border: 1px solid rgba(255, 255, 255, 0.05); + height: 100%; + box-sizing: border-box; +} + +.resource-link:hover { + transform: translateY(-2px); + box-shadow: 0 0 15px rgba(171, 87, 255, 0.15); + border-color: rgba(171, 87, 255, 0.2); +} + +.resource-emoji { + font-size: 28px; + display: flex; + align-items: center; + justify-content: center; + min-width: 36px; + background: rgba(171, 87, 255, 0.1); + height: 48px; + width: 48px; + border-radius: 12px; +} + +.resource-content { + flex: 1; +} + +.resource-title { + font-weight: 600; + color: var(--heading-color); + display: flex; + align-items: center; + gap: 8px; +} + +/* Remove the arrow decoration +.resource-title:after { + content: 'β†’'; + font-size: 0.8em; + opacity: 0; + transform: translateX(-8px); + transition: all 0.2s ease; +} + +.resource-title:hover:after { + opacity: 1; + transform: translateX(0); +} +*/ + +.resource-description { + color: var(--secondary-text); + margin: 0; + font-size: 14px; +} + +.resource-category { + margin-bottom: 48px; + padding-top: var(--spacing-md); + position: relative; +} + +/* Category accent colors - using theme variables */ +.resource-category.purple-accent { + border-left: 1px solid var(--accent-color); + padding-left: var(--spacing-md); +} + +.resource-category.blue-accent { + border-left: 1px solid #4e9fff; + padding-left: var(--spacing-md); +} + +.resource-category.green-accent { + border-left: 1px solid #57ffab; + padding-left: var(--spacing-md); +} + +.resource-category.orange-accent { + border-left: 1px solid #ff9f4e; + padding-left: var(--spacing-md); +} + +/* Enhanced section titles */ +.category-title { + font-size: 24px; + margin-bottom: var(--spacing-md); + color: var(--accent-color); + font-weight: 600; +} + +/* Grid layout for resource lists */ +.resource-list.grid-layout { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: var(--spacing-md); +} + +/* Highlighted variant - more subtle */ +.resource-link.highlighted { + border: 1px solid rgba(171, 87, 255, 0.15); + background-color: rgba(171, 87, 255, 0.02); +} + +.resource-link.highlighted:hover { + box-shadow: 0 0 15px rgba(171, 87, 255, 0.2); + border-color: rgba(171, 87, 255, 0.25); +} + +/* Compact variant */ +.resource-link.compact { + padding: var(--spacing-sm); + margin-bottom: var(--spacing-sm); +} + +.resource-link.compact .resource-emoji { + font-size: 18px; + min-width: 24px; +} + +.resource-link.compact .resource-title { + margin-bottom: 0; +} + +/* Color variant styles - more subtle */ +.resource-link.blue-glow { + border: 1px solid rgba(78, 159, 255, 0.15); + background-color: rgba(78, 159, 255, 0.02); +} + +.resource-link.blue-glow:hover { + box-shadow: 0 0 15px rgba(78, 159, 255, 0.2); + border-color: rgba(78, 159, 255, 0.25); +} + +.resource-link.green-glow { + border: 1px solid rgba(87, 255, 171, 0.15); + background-color: rgba(87, 255, 171, 0.02); +} + +.resource-link.green-glow:hover { + box-shadow: 0 0 15px rgba(87, 255, 171, 0.2); + border-color: rgba(87, 255, 171, 0.25); +} + +.resource-link.orange-glow { + border: 1px solid rgba(255, 159, 78, 0.15); + background-color: rgba(255, 159, 78, 0.02); +} + +.resource-link.orange-glow:hover { + box-shadow: 0 0 15px rgba(255, 159, 78, 0.2); + border-color: rgba(255, 159, 78, 0.25); +} + +/* Scrollable container styles */ +.scrollable-container { + position: relative; +} + +/* Remove ALL instances of the dots and gradients */ +.scrollable-container:before, +.scrollable-container:after { + display: none !important; + content: none !important; + background: none !important; +} + +/* Only enable scrolling on mobile but without dots */ +@media (max-width: 768px) { + .scrollable-container { + overflow-x: auto; + -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ + scrollbar-width: thin; /* Firefox */ + scrollbar-color: var(--accent-color) transparent; /* Firefox */ + padding-bottom: var(--spacing-md); + } + + .scrollable-container .scrollable-items { + display: flex; + gap: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-md) var(--spacing-sm) 0; + } + + .scrollable-container .resource-link { + min-width: 280px; + max-width: 350px; + flex: 0 0 auto; + } + + /* Remove fade gradient at right edge */ + .scrollable-container:before { + display: none; + } + + /* Remove arrow indicator for scroll */ + .scrollable-container:after { + display: none; + } + + /* Scrollbar styling */ + .scrollable-container::-webkit-scrollbar { + height: 6px; + } + + .scrollable-container::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 10px; + } + + .scrollable-container::-webkit-scrollbar-thumb { + background: var(--accent-color); + border-radius: 10px; + } + + /* Hide scroll buttons on mobile - now controlled by JavaScript */ + .scroll-nav-button { + display: none; + } +} + +/* Desktop grid layout */ +@media (min-width: 769px) { + .scrollable-container .scrollable-items { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--spacing-md); + } + + /* Hide scroll indicators on desktop */ + .scrollable-container:before, + .scrollable-container:after { + display: none; + } + + /* Reset resource link styles for grid layout */ + .scrollable-container .resource-link { + min-width: unset; + max-width: unset; + } +} + +/* Also remove any other instances of scrollable-container before/after */ +.scrollable-container:before, +.scrollable-container:after { + display: none; +} + +/* Extra large screens */ +@media (min-width: 1200px) { + .scrollable-container .scrollable-items { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (min-width: 1600px) { + .scrollable-container .scrollable-items { + grid-template-columns: repeat(4, 1fr); + } +} + +/* Pulse animation */ +@keyframes pulse { + 0% { + opacity: 0.7; + } + 50% { + opacity: 0.3; + } + 100% { + opacity: 0.7; + } +} + +/* Navigation buttons for horizontal scroll - now controlled by JavaScript for visibility */ +.scroll-nav-button { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 40px; + height: 40px; + background-color: rgba(171, 87, 255, 0.15); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: var(--accent-color); + font-size: 18px; + border: 1px solid rgba(171, 87, 255, 0.2); + cursor: pointer; + z-index: 10; + transition: + opacity 0.3s ease, + background-color 0.3s ease; + opacity: 0; +} + +.scrollable-container:hover .scroll-nav-button { + opacity: 0.8; +} + +.scroll-nav-button:hover { + background-color: rgba(171, 87, 255, 0.3); + opacity: 1; +} + +.scroll-nav-left { + left: -8px; +} + +.scroll-nav-right { + right: -8px; +} + +/* Default cursor styles for scrollable areas */ +.scrollable-items { + cursor: grab; +} + +.scrollable-items:active { + cursor: grabbing; +} + +/* Feature card improvements */ +.feature-card.highlighted { + box-shadow: 0 0 15px rgba(171, 87, 255, 0.15); + border: 1px solid rgba(171, 87, 255, 0.2); + background-color: rgba(171, 87, 255, 0.03); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .resource-list.grid-layout { + grid-template-columns: 1fr; + } + + .resource-category.purple-accent, + .resource-category.blue-accent, + .resource-category.green-accent, + .resource-category.orange-accent { + border-left-width: 2px; + padding-left: var(--spacing-sm); + } + + .category-title { + font-size: 20px; + } + + .resource-link { + padding: var(--spacing-sm); + } + + .scrollable-container:after { + right: 5px; + font-size: 16px; + } +} + +/* Emoji callout */ +.emoji-callout { + display: flex; + gap: var(--spacing-md); + padding: var(--spacing-md); + background-color: rgba(255, 255, 255, 0.03); + border-radius: var(--border-radius); + margin: var(--spacing-md) 0; + backdrop-filter: blur(8px); + background-color: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.emoji-callout .emoji { + font-size: 24px; + min-width: 24px; + display: flex; + align-items: center; +} + +.emoji-callout .emoji-content { + flex: 1; +} + +.emoji-callout p { + margin: 0; +} + +/* Hero improvements */ +.hero-content { + max-width: 800px; + margin: 0 auto; + text-align: center; +} + +.hero-description { + font-size: 20px; + line-height: 1.5; + margin-bottom: var(--spacing-lg); + color: var(--secondary-text); +} + +/* Coming Next Section */ +.coming-next-content { + display: flex; + flex-direction: column; + gap: var(--spacing-md); + position: relative; +} + +.coming-next-text { + font-size: 18px; + text-align: center; + margin: 0; +} + +.highlight-link { + color: var(--accent-color); + font-weight: 600; + position: relative; + display: inline-block; + padding: 0 2px; +} + +.highlight-link:after { + content: ""; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 2px; + background: var(--accent-gradient); + transform: scaleX(0); + transition: transform var(--transition-speed); + transform-origin: right; +} + +.highlight-link:hover:after { + transform: scaleX(1); + transform-origin: left; +} + +.volunteer-note { + display: flex; + align-items: center; + gap: var(--spacing-md); + background-color: rgba(171, 87, 255, 0.05); + padding: var(--spacing-md); + border-radius: var(--border-radius); + border-left: 3px solid var(--accent-color); +} + +.volunteer-note .emoji { + font-size: 24px; +} + +.volunteer-note p { + margin: 0; + color: var(--secondary-text); +} + +/* Resource lists */ +.resource-list { + list-style: none; + padding: 0; + margin: 0; +} + +/* Enhanced scrollable containers */ +.scrollable-container { + position: relative; +} + +.scrollable-container:before, +.scrollable-container:after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + width: 40px; + pointer-events: none; + z-index: 2; +} + +.scrollable-container:before { + left: 0; + background: linear-gradient(90deg, var(--background), transparent); +} + +.scrollable-container:after { + right: 0; + background: linear-gradient(270deg, var(--background), transparent); +} + +/* Improved scroll buttons */ +.scroll-nav-button { + width: 40px; + height: 40px; + background-color: rgba(171, 87, 255, 0.15); + border-radius: 50%; + opacity: 0; + transition: + opacity 0.3s ease, + background-color 0.3s ease; + z-index: 10; +} + +.scrollable-container:hover .scroll-nav-button { + opacity: 0.8; +} + +.scroll-nav-button:hover { + background-color: rgba(171, 87, 255, 0.3); + opacity: 1; +} + +/* Mobile swipe indicator */ +@media (max-width: 768px) { + .scrollable-container:after { + content: "β†’"; + background: none; + display: flex; + align-items: center; + justify-content: flex-end; + padding-right: 15px; + color: var(--accent-color); + font-size: 20px; + animation: pulse-hint 2s infinite ease-in-out; + } + + @keyframes pulse-hint { + 0% { + transform: translateX(0); + opacity: 0.7; + } + 50% { + transform: translateX(5px); + opacity: 1; + } + 100% { + transform: translateX(0); + opacity: 0.7; + } + } +} + +/* Eager hover state for resource links */ +.resource-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--spacing-md); +} + +.resource-list li:hover + li .resource-link, +.resource-list li:has(+ li:hover) .resource-link { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(171, 87, 255, 0.1); +} + +/* CTA and Contribute section enhancements */ +.cta_button { + position: relative; + overflow: hidden; +} + +.cta_button:before { + content: ""; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: rgba(255, 255, 255, 0.1); + transform: rotate(45deg); + animation: button-shine 3s infinite ease-in-out; + z-index: 1; +} + +@keyframes button-shine { + 0% { + transform: translateX(-100%) rotate(45deg); + } + 30%, + 100% { + transform: translateX(100%) rotate(45deg); + } +} + +.cta_button span { + position: relative; + z-index: 2; +} + +/* Coming Next section */ +.coming-next-content { + position: relative; +} + +/* Masonry-style grid for varying card heights */ +@media (min-width: 768px) { + .resource-list.masonry-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + grid-auto-rows: 10px; + grid-gap: var(--spacing-md); + } + + .resource-list.masonry-grid .resource-link { + grid-row-end: span calc(var(--card-height, 100) / 10); + } +} + +/* Elevated section styles */ +.content-section.elevated-section { + background-color: rgba(171, 87, 255, 0.03); + border-top: 1px solid rgba(171, 87, 255, 0.1); + border-bottom: 1px solid rgba(171, 87, 255, 0.1); +} + +/* Blurred emoji callout */ +.emoji-callout.blurred { + backdrop-filter: blur(8px); + background-color: rgba(255, 255, 255, 0.05); + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1); +} + +/* Animated CTA cards */ +.content-card.animate-cta .cta_button, +.content-card.animate-cta .secondary-button { + position: relative; + overflow: hidden; +} + +.content-card.animate-cta .cta_button:before { + content: ""; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: rgba(255, 255, 255, 0.1); + transform: rotate(45deg); + animation: button-shine 3s infinite ease-in-out; + z-index: 1; +} + +@keyframes button-shine { + 0% { + transform: translateX(-100%) rotate(45deg); + } + 30%, + 100% { + transform: translateX(100%) rotate(45deg); + } +} + +.content-card.animate-cta .cta_button span { + position: relative; + z-index: 2; +} + +/* Feature card emoji styling */ +.feature-emoji { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 20px; + margin-right: 8px; + color: var(--accent-color); +} + +/* Card variants - make all cards visually equivalent */ +.resource-link { + display: flex; + gap: var(--spacing-md); + padding: var(--spacing-md); + background-color: var(--card-bg); + border-radius: var(--border-radius); + margin-bottom: var(--spacing-md); + transition: all var(--transition-speed); + border: 1px solid rgba(255, 255, 255, 0.05); + height: 100%; + box-sizing: border-box; +} + +.resource-link:hover { + transform: translateY(-2px); + box-shadow: 0 0 15px rgba(171, 87, 255, 0.15); + border-color: rgba(171, 87, 255, 0.2); +} + +/* Completely neutralize variant styles */ +.resource-link.highlighted, +.resource-link.blue-glow, +.resource-link.green-glow, +.resource-link.orange-glow { + border: 1px solid rgba(255, 255, 255, 0.05); + background-color: var(--card-bg); + box-shadow: none; +} + +/* All variants use the same hover style */ +.resource-link.highlighted:hover, +.resource-link.blue-glow:hover, +.resource-link.green-glow:hover, +.resource-link.orange-glow:hover { + transform: translateY(-2px); + box-shadow: 0 0 15px rgba(171, 87, 255, 0.15); + border-color: rgba(171, 87, 255, 0.2); +} + +/* Final override to remove ALL dots, arrows, and other decorative elements from scrollable containers */ +.scrollable-container:before, +.scrollable-container:after { + display: none !important; + content: none !important; + width: 0 !important; + height: 0 !important; + background: none !important; + opacity: 0 !important; + pointer-events: none !important; + border: none !important; + box-shadow: none !important; +} + +/* Additional responsive styles */ +@media (max-width: 768px) { + /* General responsive styles */ + .container { + padding: 0 var(--spacing-sm); + } + + /* Hero section responsive adjustments */ + .hero h1 { + font-size: clamp(36px, 8vw, 64px); + } + + .hero p { + font-size: 16px; + } + + .buttons-container { + flex-direction: column; + gap: var(--spacing-sm); + width: 100%; + } + + .cta_button, + .secondary-button { + text-align: center; + } + + /* Features grid responsive layout */ + .features-grid { + grid-template-columns: 1fr; + gap: var(--spacing-md); + } + + /* Footer responsive layout */ + footer .footer-grid { + grid-template-columns: 1fr; + gap: var(--spacing-md); + } + + footer .footer-bottom { + flex-direction: column; + text-align: center; + } + + /* Make section titles smaller on mobile */ + .section-title { + font-size: 32px; + } + + /* Adjust emoji callout layout for mobile */ + .emoji-callout { + flex-direction: column; + padding: var(--spacing-sm); + } + + .emoji-callout .emoji { + margin-bottom: var(--spacing-sm); + } + + /* Ensure content card padding is appropriate for mobile */ + .content-card { + padding: var(--spacing-sm); + } + + /* Fix any potential horizontal overflow issues */ + html, + body { + width: 100%; + } +} + +/* Resource grid styles */ +.resource-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--spacing-lg); + margin: 1.5rem 0; +} + +/* Animations */ +@keyframes pulse-bg { + 0% { + opacity: 0.5; + transform: scale(1); + } + 100% { + opacity: 0.8; + transform: scale(1.1); + } +} + +/* Button animations */ +.cta_button { + position: relative; + overflow: hidden; +} + +.cta_button:before { + content: ""; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: rgba(255, 255, 255, 0.1); + transform: rotate(45deg); + animation: button-shine 3s infinite ease-in-out; + z-index: 1; +} + +@keyframes button-shine { + 0% { + transform: translateX(-100%) rotate(45deg); + } + 30%, + 100% { + transform: translateX(100%) rotate(45deg); + } +} + +.cta_button span { + position: relative; + z-index: 2; +} + +/* Hero section animation */ +.hero:before { + content: ""; + position: absolute; + top: -20%; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 50% 30%, rgba(171, 87, 255, 0.03) 0%, transparent 70%); + animation: pulse-bg 10s infinite alternate ease-in-out; } diff --git a/web/js/clickable-cards.js b/web/js/clickable-cards.js new file mode 100644 index 0000000..104c9c2 --- /dev/null +++ b/web/js/clickable-cards.js @@ -0,0 +1,51 @@ +// Make resource-link cards fully clickable +document.addEventListener('DOMContentLoaded', function() { + console.log('[ClickableCard] DOMContentLoaded event fired.'); + const resourceLinks = document.querySelectorAll('.resource-link'); + console.log(`[ClickableCard] Found ${resourceLinks.length} elements with class .resource-link.`); + + resourceLinks.forEach(function(card, index) { + console.log(`[ClickableCard] Processing card #${index + 1}`, card); + const titleLink = card.querySelector('a.resource-title'); + + if (titleLink) { + console.log(`[ClickableCard] Card #${index + 1}: Found titleLink:`, titleLink, `with href: ${titleLink.getAttribute('href')}`); + const url = titleLink.getAttribute('href'); + const targetAttribute = titleLink.getAttribute('target'); + const target = targetAttribute || '_self'; + console.log(`[ClickableCard] Card #${index + 1}: URL='${url}', Target='${target}' (from attribute '${targetAttribute}')`); + + if (!url) { + console.warn(`[ClickableCard] Card #${index + 1}: titleLink found, but href attribute is missing or empty. Card will not be fully clickable.`); + return; // Skip adding listener if URL is invalid + } + + card.addEventListener('click', function(event) { + const clickedElement = event.target; + console.log(`[ClickableCard] Card #${index + 1} clicked!`); + console.log(`[ClickableCard] event.target (actual clicked element):`, clickedElement); + console.log(`[ClickableCard] titleLink (the main in this card):`, titleLink); + + // If the click target (or its closest ancestor) is an element with the class 'a.resource-title', + // let the browser handle the navigation. + if (clickedElement.closest('a.resource-title')) { + console.log(`[ClickableCard] Decision: Click was on or within a.resource-title. Allowing default browser navigation.`); + return; + } + + // Otherwise, the click was on the card's empty space. Navigate using JS. + console.log(`[ClickableCard] Decision: Click was NOT on or within a.resource-title. Attempting JS navigation to ${url}.`); + if (target === '_blank') { + window.open(url, '_blank'); + } else { + window.location.href = url; + } + }); + // card.style.cursor = 'pointer'; // This is handled by CSS + console.log(`[ClickableCard] Card #${index + 1}: Click listener attached.`); + } else { + console.warn(`[ClickableCard] Card #${index + 1}: Could not find a.resource-title within this card. It will not be fully clickable.`); + } + }); + console.log('[ClickableCard] Script initialization finished.'); +}); diff --git a/web/js/scroll.js b/web/js/scroll.js new file mode 100644 index 0000000..e15a55d --- /dev/null +++ b/web/js/scroll.js @@ -0,0 +1,77 @@ +document.addEventListener('DOMContentLoaded', function() { + // Handle all scrollable containers + const containers = document.querySelectorAll('.scrollable-container'); + + containers.forEach(container => { + const leftButton = container.querySelector('.scroll-nav-left'); + const rightButton = container.querySelector('.scroll-nav-right'); + const scrollableItems = container.querySelector('.scrollable-items'); + + if (leftButton && rightButton && scrollableItems) { + // Check if we should show buttons based on overflow + if (container.classList.contains('dynamic-scroll-buttons')) { + const hasOverflow = scrollableItems.scrollWidth > scrollableItems.clientWidth; + leftButton.style.display = hasOverflow ? 'flex' : 'none'; + rightButton.style.display = hasOverflow ? 'flex' : 'none'; + + // Add resize listener + window.addEventListener('resize', function() { + const hasOverflowOnResize = scrollableItems.scrollWidth > scrollableItems.clientWidth; + leftButton.style.display = hasOverflowOnResize ? 'flex' : 'none'; + rightButton.style.display = hasOverflowOnResize ? 'flex' : 'none'; + }); + } + + // Add click handlers + leftButton.addEventListener('click', () => { + scrollableItems.scrollBy({ left: -300, behavior: 'smooth' }); + }); + + rightButton.addEventListener('click', () => { + scrollableItems.scrollBy({ left: 300, behavior: 'smooth' }); + }); + + // Enable drag to scroll + let isDown = false; + let startX; + let scrollLeft; + + scrollableItems.addEventListener('mousedown', (e) => { + isDown = true; + scrollableItems.style.cursor = 'grabbing'; + startX = e.pageX - scrollableItems.offsetLeft; + scrollLeft = scrollableItems.scrollLeft; + }); + + scrollableItems.addEventListener('mouseleave', () => { + isDown = false; + scrollableItems.style.cursor = 'grab'; + }); + + scrollableItems.addEventListener('mouseup', () => { + isDown = false; + scrollableItems.style.cursor = 'grab'; + }); + + scrollableItems.addEventListener('mousemove', (e) => { + if (!isDown) return; + e.preventDefault(); + const x = e.pageX - scrollableItems.offsetLeft; + const walk = (x - startX) * 2; // Scroll speed + scrollableItems.scrollLeft = scrollLeft - walk; + }); + } + }); + + // Process elements with data-onrender attribute + document.querySelectorAll('[data-onrender]').forEach(function(el) { + try { + const scriptContent = el.getAttribute('data-onrender'); + if (scriptContent) { + new Function(scriptContent)(); + } + } catch (e) { + console.error('Error executing onrender script:', e); + } + }); +}); \ No newline at end of file diff --git a/web/main.dart b/web/main.dart index fdf5358..3351c28 100644 --- a/web/main.dart +++ b/web/main.dart @@ -1,7 +1,84 @@ import 'package:fc_ai_circle/fc_ai_circle.dart'; import 'package:jaspr/browser.dart'; +import 'dart:js' as js; void main() { Jaspr.initializeApp(); runApp(Website()); + + // Enhanced navigation and scroll fix for the app + js.context.callMethod('eval', [ + ''' + (function() { + // Reset scroll position on all navigation events + function resetScrollPosition() { + window.scrollTo({ + top: 0, + left: 0, + behavior: 'auto' + }); + } + + // Apply scroll reset on page load + document.addEventListener('DOMContentLoaded', function() { + resetScrollPosition(); + + // Global click handler for all link navigation + document.addEventListener('click', function(e) { + const link = e.target.closest('a'); + if (link) { + const href = link.getAttribute('href'); + + // Handle nav menu link clicks - close the mobile menu + if (link.classList.contains('nav-link')) { + // Find the nav-links element and remove the 'open' class + const navLinks = document.querySelector('.nav-links'); + if (navLinks && navLinks.classList.contains('open')) { + navLinks.classList.remove('open'); + } + } + + if (href && (href.includes('/starters') || href.includes('/builders'))) { + // For internal links, set a flag to scroll after navigation + window.__needsScrollReset = true; + } + } + }); + }); + + // Handle router navigation events + const originalPushState = history.pushState; + history.pushState = function() { + originalPushState.apply(this, arguments); + if (window.__needsScrollReset) { + setTimeout(resetScrollPosition, 10); + window.__needsScrollReset = false; + } + }; + + // Handle back/forward navigation + window.addEventListener('popstate', resetScrollPosition); + + // Check periodically for any scrolling issues after navigation + // (helps catch cases where the router takes time to render) + window.addEventListener('load', function() { + setTimeout(resetScrollPosition, 100); + setTimeout(resetScrollPosition, 500); + }); + + // Override internal router navigation to ensure scrolling works + const checkForRouterNavigation = setInterval(function() { + const path = window.location.pathname; + if (path.includes('/starters') || path.includes('/builders')) { + resetScrollPosition(); + } + }, 200); + + // Clear the interval after 2 seconds (navigation should be complete by then) + setTimeout(function() { + clearInterval(checkForRouterNavigation); + }, 2000); + })(); + ''' + ]); }