From a4583b8e8e2b0e029bc15bf81b3e96a85dc899c1 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 20:23:03 +0200 Subject: [PATCH 01/31] fix: spread operator added for website routes --- lib/src/app/website.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/app/website.dart b/lib/src/app/website.dart index 93d32d3..c39389a 100644 --- a/lib/src/app/website.dart +++ b/lib/src/app/website.dart @@ -24,8 +24,8 @@ class _SiteState extends State { yield Router( routes: [ ...HomePage.route(), - StartersPage.route(), - BuildersPage.route(), + ...StartersPage.route(), + ...BuildersPage.route(), ], ); } From 476eb77a0910fa1a1c4dbebedea4e894b999dff8 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 20:35:05 +0200 Subject: [PATCH 02/31] chore: add launch configuration for Dart in VSCode --- .vscode/launch.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9eb25d6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // 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": "ai_builders_circle", + "request": "launch", + "type": "dart", + "program": "web/main.dart" + } + ] +} \ No newline at end of file From 92937f57e6e8f550b2aaf7a0638f24f9eaa8feed Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 20:35:41 +0200 Subject: [PATCH 03/31] fix: update hero section title structure in home page --- lib/src/pages/home_page.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home_page.dart index b98b59e..1390ff1 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/pages/home_page.dart @@ -42,7 +42,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 ' From f061d59f67a51bbb57488e74e8b45bc06cb5f058 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 20:36:08 +0200 Subject: [PATCH 04/31] refactor: enhance Builders and Starters pages with structured sections and improved routing * Change path variables to constants for better clarity. * Refactor route methods to use generator functions for improved performance. * Introduce new sections (_HeroSection, _AboutSection, _MissionSection, _WhatWeDoSection, _JoinSection, _ContributorsSection) in BuildersPage. * Add structured sections (_HeroSection, _CoreConceptsSection, _OfficialDocsSection, _DeepenUnderstandingSection, _ContributeSection, _ComingNextSection) in StartersPage. * Update styles for category sections and improve focus visibility in CSS. --- lib/src/pages/builders_page.dart | 213 ++++++++++++++- lib/src/pages/starters_page.dart | 433 ++++++++++++++++++++++++++++++- web/css/styles.css | 125 +++++++++ 3 files changed, 752 insertions(+), 19 deletions(-) diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index 83005b6..008629a 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -5,12 +5,12 @@ import 'package:jaspr_router/jaspr_router.dart'; class BuildersPage extends StatelessComponent { const BuildersPage({super.key}); - static var path = '/builders'; + static const path = '/builders'; - static Route route() { - return Route( + static Iterable route() sync* { + yield Route( path: path, - title: 'Builders', + title: 'Builders - Flutter AI Builders Circle', builder: (BuildContext context, RouteState state) => BuildersPage(), ); } @@ -19,17 +19,202 @@ 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') + _HeroSection(), + _AboutSection(), + _MissionSection(), + _WhatWeDoSection(), + _JoinSection(), + _ContributorsSection(), + ], + ); + } +} + +class _HeroSection extends StatelessComponent { + const _HeroSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + classes: 'hero', + attributes: {'role': 'banner'}, + [ + div(classes: 'container', [ + h1( + classes: 'page-title', + [ + span( + classes: 'title-text', + [text('Builders')], + ), + ], + ), + p([ + text( + 'An open-source, volunteer-led community focused on integrating AI capabilities ' + 'into Flutter applications.', + ) + ]), + ]), + ], + ); + } +} + +class _AboutSection extends StatelessComponent { + const _AboutSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'about', + attributes: {'role': 'region', 'aria-label': 'About Us'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('About Us')]), + p([ + text( + '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).', + ) + ]), + ]), + ], + ); + } +} + +class _MissionSection extends StatelessComponent { + const _MissionSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'mission', + attributes: {'role': 'region', 'aria-label': 'Our Mission'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('Our Mission 🎯')]), + ul([ + li([ + text( + 'Create accessible resources for Flutter developers to incorporate AI into their apps', + ) + ]), + 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')]), + ]), + ]), + ], + ); + } +} + +class _WhatWeDoSection extends StatelessComponent { + const _WhatWeDoSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'what-we-do', + attributes: {'role': 'region', 'aria-label': 'What We Do'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('What We Do 📦')]), + div( + classes: 'features-grid', + attributes: {'role': 'list'}, + [ + _FeatureCard( + title: 'Share Knowledge', + description: 'Technical articles, code examples, and implementation guides', + ), + _FeatureCard( + title: 'Build Tools', + description: 'Open-source packages and utilities to simplify AI integration', + ), + _FeatureCard( + title: 'Provide Templates', + description: 'Ready-to-use starter projects for common AI tasks', + ), + _FeatureCard( + title: 'Foster Collaboration', + description: 'Connect developers working on similar challenges', + ), + ], + ), + ]), + ], + ); + } +} + +class _FeatureCard extends StatelessComponent { + const _FeatureCard({ + required this.title, + required this.description, + }); + + final String title; + final String description; + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: 'feature-card', + attributes: {'role': 'listitem'}, + [ + h3([text(title)]), + p([text(description)]), + ], + ); + } +} + +class _JoinSection extends StatelessComponent { + const _JoinSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'join', + attributes: {'role': 'region', 'aria-label': 'Join Us'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('Join Us')]), + p([ + text( + 'We\'re currently preparing our community platforms. Join us to collaborate, ' + 'learn, and build the future of AI-powered Flutter apps.', + ) + ]), + ]), + ], + ); + } +} + +class _ContributorsSection extends StatelessComponent { + const _ContributorsSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'contributors', + attributes: {'role': 'region', 'aria-label': 'Want to Contribute'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('Want to Contribute? 🔗')]), + p([ + text( + '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.', + ) ]), - 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')]), + p([text('PRs welcome - watch this space for contribution guidelines.')]), ]), ], ); diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index c2cd040..e3bf025 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -5,12 +5,12 @@ 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 +19,430 @@ class StartersPage extends StatelessComponent { Iterable build(BuildContext context) sync* { yield PageLayout( children: [ - h1([text('Starters')]), + _HeroSection(), + _CoreConceptsSection(), + _OfficialDocsSection(), + _DeepenUnderstandingSection(), + _ContributeSection(), + _ComingNextSection(), + ], + ); + } +} + +class _HeroSection extends StatelessComponent { + const _HeroSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + classes: 'hero', + attributes: {'role': 'banner'}, + [ + div(classes: 'container', [ + h1( + classes: 'page-title', + [ + span( + classes: 'title-text', + [text('Starters')], + ), + ], + ), + p([ + text( + '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.', + ) + ]), + blockquote([ + text( + '🧠 We believe in learning by doing — and starting with official sources. ' + 'All links below come from official documentation or deeply trusted platforms.', + ) + ]), + ]), + ], + ); + } +} + +class _CoreConceptsSection extends StatelessComponent { + const _CoreConceptsSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'core-concepts', + attributes: {'role': 'region', 'aria-label': 'Core Concepts'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('🧩 Core Concepts You Should Know')]), + h3([text('What is an Agentic App?')]), + ul( + attributes: {'role': 'list'}, + [ + _ResourceItem( + emoji: '🧠', + title: 'LangChain: Introduction', + description: + 'Learn the foundations of LangChain and its modular architecture for building LLM-powered agents.', + link: 'https://python.langchain.com/docs/introduction/', + ), + _ResourceItem( + emoji: '📘', + title: 'What is Agentic AI? (IBM)', + description: + 'Overview of how agentic AI differs from traditional AI, with real-world examples.', + link: + 'https://www.ibm.com/think/topics/agentic-ai#:~:text=Agentic%20AI%20solutions%20can%20be,predictive%20analytics%20and%20execute%20trades.', + ), + _ResourceItem( + emoji: '🔍', + title: 'Introducing the Model Context Protocol (Anthropic)', + description: 'Learn how MCP standardizes interactions between LLMs and tools.', + link: 'https://www.anthropic.com/news/model-context-protocol', + ), + _ResourceItem( + emoji: '📚', + title: 'Amazon AWS: What is RAG (Retrieval-Augmented Generation)?', + description: + 'A simple primer on Retrieval-Augmented Generation and how it improves model accuracy.', + link: 'https://aws.amazon.com/what-is/retrieval-augmented-generation/', + ), + _ResourceItem( + emoji: '🎥', + title: 'Observable Flutter #59 – Agentic Apps with Flutter', + description: + 'Craig Labenz and Andrew Brogdon walk through agentic patterns, Colorist, and Firebase Genkit.', + link: + 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', + ), + ], + ), + ]), + ], + ); + } +} + +class _ResourceItem extends StatelessComponent { + const _ResourceItem({ + required this.emoji, + required this.title, + required this.description, + required this.link, + }); + + final String emoji; + final String title; + final String description; + final String link; + + @override + Iterable build(BuildContext context) sync* { + yield li( + attributes: {'role': 'listitem'}, + [ + text('$emoji '), + a( + href: link, + target: Target.blank, + attributes: { + 'aria-label': '$title - $description', + 'rel': 'noopener noreferrer', + }, + [text(title)], + ), + text(' – $description'), + ], + ); + } +} + +class _OfficialDocsSection extends StatelessComponent { + const _OfficialDocsSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'official-docs', + attributes: {'role': 'region', 'aria-label': 'Official Documentation'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('🔗 Official Docs & Tools Worth Exploring')]), + _CategorySection( + title: 'LLM Integration (Gemini, OpenAI, Claude)', + resources: [ + _ResourceItem( + emoji: '🎨', + title: 'Gemini API + Flutter (Colorist Codelab)', + description: + 'Beginner/Intermediate. Learn how to integrate Gemini into a Flutter app using Firebase extensions and function calling.', + link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', + ), + _ResourceItem( + emoji: '🤖', + title: 'OpenAI API Docs', + description: + 'Beginner. Covers completions, chat, and function calling for GPT models.', + link: 'https://platform.openai.com/docs', + ), + _ResourceItem( + emoji: '📦', + title: 'dart_openai (Community SDK)', + description: + 'Intermediate. A Dart package that simplifies using OpenAI APIs within Flutter projects.', + link: 'https://pub.dev/packages/dart_openai', + ), + _ResourceItem( + emoji: '🧠', + title: 'Anthropic Claude API', + description: + 'Beginner. Official Claude API docs with example request/response flows.', + link: 'https://docs.anthropic.com/claude/docs', + ), + ], + ), + _CategorySection( + title: 'Speech-to-Text (Whisper)', + resources: [ + _ResourceItem( + emoji: '🎤', + title: 'OpenAI Whisper API', + description: + 'Beginner. Transcribe audio to text using OpenAI\'s hosted Whisper model.', + link: 'https://platform.openai.com/docs/guides/speech-to-text', + ), + _ResourceItem( + emoji: '📱', + title: 'flutter_whisper.cpp', + description: + 'Advanced. Community-maintained Flutter app running Whisper on-device via Rust FFI and whisper.cpp.', + link: 'https://github.com/lyledean1/flutter_whisper.cpp', + ), + ], + ), + _CategorySection( + title: 'Firebase Vector Search & RAG', + resources: [ + _ResourceItem( + emoji: '🔍', + title: 'Firestore Vector Search (Extension)', + description: + 'Intermediate. Store and retrieve vector embeddings in Firestore for building RAG pipelines.', + link: 'https://extensions.dev/extensions/googlecloud/firestore-vector-search', + ), + _ResourceItem( + emoji: '⚡', + title: 'Firebase Genkit', + description: + 'Advanced. A backend framework for building AI-driven workflows with Gemini, designed for Cloud Functions.', + link: 'https://firebase.google.com/docs/genkit', + ), + ], + ), + _CategorySection( + title: 'MCP (Model Context Protocol)', + resources: [ + _ResourceItem( + emoji: '🔄', + title: 'Model Context Protocol Overview', + description: + 'Advanced. MCP is an open protocol to connect LLMs with tools and data providers.', + link: 'https://modelcontextprotocol.io/introduction', + ), + ], + ), + _CategorySection( + title: 'Local LLMs (Ollama)', + resources: [ + _ResourceItem( + emoji: '🏠', + title: 'Ollama', + description: + 'Intermediate. Easily run LLMs like LLaMA or Mistral locally and serve them via HTTP.', + link: 'https://github.com/ollama/ollama', + ), + ], + ), + _CategorySection( + title: 'Agentic Patterns', + resources: [ + _ResourceItem( + emoji: '🔧', + title: 'Function Calling (OpenAI)', + description: + 'Intermediate. Enables models to call structured functions from your app.', + link: 'https://platform.openai.com/docs/guides/function-calling', + ), + _ResourceItem( + emoji: '🤖', + title: 'LLM Agents (Prompt Engineering Guide)', + description: 'Advanced. Explore ReAct and other reasoning/action agent patterns.', + link: 'https://www.promptingguide.ai/techniques/react', + ), + ], + ), + _CategorySection( + title: 'Codelabs, Templates, and Examples', + resources: [ + _ResourceItem( + emoji: '🎨', + title: 'Colorist (Gemini + Flutter)', + description: + 'Intermediate. Gemini-powered Flutter app that updates the UI based on natural language prompts.', + link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', + ), + _ResourceItem( + emoji: '📚', + title: 'Gemini API Cookbook (GitHub)', + description: + 'Intermediate/Advanced. Google\'s official examples for calling Gemini API across languages including Dart.', + link: 'https://github.com/google-gemini/cookbook', + ), + ], + ), + _CategorySection( + title: 'Dart AI Libraries', + resources: [ + _ResourceItem( + emoji: '🔗', + title: 'LangChain.dart', + description: + 'Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more.', + link: 'https://pub.dev/packages/langchain', + ), + ], + ), + ]), + ], + ); + } +} + +class _CategorySection extends StatelessComponent { + const _CategorySection({ + required this.title, + required this.resources, + }); + + final String title; + final List<_ResourceItem> resources; + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: 'category-section', + attributes: {'role': 'region', 'aria-label': title}, + [ + h3([text(title)]), + ul( + attributes: {'role': 'list'}, + resources, + ), + ], + ); + } +} + +class _DeepenUnderstandingSection extends StatelessComponent { + const _DeepenUnderstandingSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'deepen-understanding', + attributes: {'role': 'region', 'aria-label': 'Deepen Your Understanding'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('🧠 Want to Deepen Your Understanding?')]), + ul( + attributes: {'role': 'list'}, + [ + _ResourceItem( + emoji: '🎥', + title: 'Agentic Apps with Flutter (Observable Flutter #59)', + description: '', + link: + 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', + ), + _ResourceItem( + emoji: '💬', + title: 'Gemini Function Calling Walkthrough (Google I/O)', + description: '', + link: 'https://ai.google.dev/gemini-api/docs/function-calling', + ), + ], + ), + ]), + ], + ); + } +} + +class _ContributeSection extends StatelessComponent { + const _ContributeSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'contribute', + attributes: {'role': 'region', 'aria-label': 'Contribute'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('🤝 Contribute or Join the Circle')]), + 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([text('👉 **Open a PR or stay tuned for our Slack/Discord — coming soon!**')]), + ]), + ], + ); + } +} + +class _ComingNextSection extends StatelessComponent { + const _ComingNextSection(); + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: 'coming-next', + attributes: {'role': 'region', 'aria-label': 'Coming Next'}, + [ + div(classes: 'container', [ + h2(classes: 'section-title', [text('🧪 Coming Next')]), + p([ + text('Stay tuned at '), + a( + href: 'https://ai.fluttercommunity.dev', + target: Target.blank, + attributes: { + 'aria-label': 'Visit ai.fluttercommunity.dev', + 'rel': 'noopener noreferrer', + }, + [text('ai.fluttercommunity.dev')], + ), + ]), + p([ + text( + '🌱 *This space is volunteer-built with ❤️ — we aim to make agentic Flutter development more accessible for everyone.*', + ) + ]), + ]), ], ); } diff --git a/web/css/styles.css b/web/css/styles.css index 59a4005..62de819 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -379,3 +379,128 @@ footer a:hover, a.selected { } */ } + +/* 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; +} + +/* 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; +} +*/ From bac07897f9f83798b7007b129871b41d8280a5ed Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 21:03:58 +0200 Subject: [PATCH 05/31] feat: introduce new content components for enhanced page structure * Add ContentSection, ContentCard, and EmojiCallout components for better modularity. * Refactor BuildersPage and StartersPage to utilize new components for improved readability and maintainability. * Update sections to use structured components, enhancing the overall layout and user experience. --- lib/src/components/content_section.dart | 93 ++++ lib/src/components/page_hero.dart | 42 ++ lib/src/components/resource_link.dart | 78 +++ lib/src/pages/builders_page.dart | 266 ++++----- lib/src/pages/starters_page.dart | 695 +++++++++++------------- 5 files changed, 632 insertions(+), 542 deletions(-) create mode 100644 lib/src/components/content_section.dart create mode 100644 lib/src/components/page_hero.dart create mode 100644 lib/src/components/resource_link.dart diff --git a/lib/src/components/content_section.dart b/lib/src/components/content_section.dart new file mode 100644 index 0000000..84013e4 --- /dev/null +++ b/lib/src/components/content_section.dart @@ -0,0 +1,93 @@ +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, + required this.children, + }); + + final String title; + final String? id; + final String? emoji; + final String? description; + final bool contentPadding; + final List children; + + @override + Iterable build(BuildContext context) sync* { + yield section( + id: id, + classes: 'content-section', + attributes: {'role': 'region', 'aria-label': title}, + [ + div( + classes: 'container', + [ + h2( + classes: 'section-title', + [ + if (emoji != null) 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, + }); + + final List children; + final bool elevated; + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: ['content-card', if (elevated) 'elevated'].join(' '), + children, + ); + } +} + +class EmojiCallout extends StatelessComponent { + const EmojiCallout({ + super.key, + required this.emoji, + required this.children, + }); + + final String emoji; + final List children; + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: 'emoji-callout', + [ + span(classes: 'emoji', [text(emoji)]), + div(classes: 'emoji-content', children), + ], + ); + } +} 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_link.dart b/lib/src/components/resource_link.dart new file mode 100644 index 0000000..db4b0ce --- /dev/null +++ b/lib/src/components/resource_link.dart @@ -0,0 +1,78 @@ +import 'package:jaspr/browser.dart'; + +class ResourceLink extends StatelessComponent { + const ResourceLink({ + super.key, + required this.emoji, + required this.title, + required this.link, + this.description, + }); + + final String emoji; + final String title; + final String link; + final String? description; + + @override + Iterable build(BuildContext context) sync* { + yield li( + attributes: {'role': 'listitem'}, + [ + div( + classes: 'resource-link', + [ + span(classes: 'resource-emoji', [text(emoji)]), + div( + classes: 'resource-content', + [ + a( + href: link, + target: Target.blank, + attributes: { + 'aria-label': '$title - ${description ?? ''}', + 'rel': 'noopener noreferrer', + }, + classes: 'resource-title', + [text(title)], + ), + if (description != null) + p( + classes: 'resource-description', + [text(description!)], + ), + ], + ), + ], + ), + ], + ); + } +} + +class ResourceCategory extends StatelessComponent { + const ResourceCategory({ + super.key, + required this.title, + required this.resources, + }); + + final String title; + final List resources; + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: 'resource-category', + attributes: {'role': 'region', 'aria-label': title}, + [ + h3(classes: 'category-title', [text(title)]), + ul( + classes: 'resource-list', + attributes: {'role': 'list'}, + resources, + ), + ], + ); + } +} diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index 008629a..d2943f0 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -1,3 +1,5 @@ +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/layouts/page_layout.dart'; import 'package:jaspr/browser.dart'; import 'package:jaspr_router/jaspr_router.dart'; @@ -19,133 +21,137 @@ class BuildersPage extends StatelessComponent { Iterable build(BuildContext context) sync* { yield PageLayout( children: [ - _HeroSection(), - _AboutSection(), - _MissionSection(), - _WhatWeDoSection(), - _JoinSection(), - _ContributorsSection(), + _buildHeroSection(), + _buildAboutSection(), + _buildMissionSection(), + _buildWhatWeDoSection(), + _buildJoinSection(), + _buildContributorsSection(), ], ); } -} -class _HeroSection extends StatelessComponent { - const _HeroSection(); + Component _buildHeroSection() { + return PageHero( + title: 'Builders', + description: 'An open-source, volunteer-led community focused on integrating AI capabilities ' + 'into Flutter applications.', + ); + } - @override - Iterable build(BuildContext context) sync* { - yield section( - classes: 'hero', - attributes: {'role': 'banner'}, - [ - div(classes: 'container', [ - h1( - classes: 'page-title', - [ - span( - classes: 'title-text', - [text('Builders')], - ), - ], - ), - p([ - text( - 'An open-source, volunteer-led community focused on integrating AI capabilities ' - 'into Flutter applications.', - ) - ]), - ]), + Component _buildAboutSection() { + return ContentSection( + title: 'About Us', + id: 'about', + children: [ + ContentCard( + children: [ + p([ + text( + '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).', + ) + ]), + ], + ), ], ); } -} -class _AboutSection extends StatelessComponent { - const _AboutSection(); - - @override - Iterable build(BuildContext context) sync* { - yield section( - id: 'about', - attributes: {'role': 'region', 'aria-label': 'About Us'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('About Us')]), - p([ - text( - '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).', - ) - ]), - ]), + Component _buildMissionSection() { + return ContentSection( + title: 'Our Mission', + id: 'mission', + emoji: '🎯', + children: [ + ContentCard( + children: [ + ul([ + li([ + text( + 'Create accessible resources for Flutter developers to incorporate AI into their apps', + ) + ]), + 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')]), + ]), + ], + ), ], ); } -} -class _MissionSection extends StatelessComponent { - const _MissionSection(); + 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', + ), + _FeatureCard( + title: 'Build Tools', + description: 'Open-source packages and utilities to simplify AI integration', + ), + _FeatureCard( + title: 'Provide Templates', + description: 'Ready-to-use starter projects for common AI tasks', + ), + _FeatureCard( + title: 'Foster Collaboration', + description: 'Connect developers working on similar challenges', + ), + ], + ), + ], + ); + } - @override - Iterable build(BuildContext context) sync* { - yield section( - id: 'mission', - attributes: {'role': 'region', 'aria-label': 'Our Mission'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('Our Mission 🎯')]), - ul([ - li([ + Component _buildJoinSection() { + return ContentSection( + title: 'Join Us', + id: 'join', + children: [ + ContentCard( + children: [ + p([ text( - 'Create accessible resources for Flutter developers to incorporate AI into their apps', + 'We\'re currently preparing our community platforms. Join us to collaborate, ' + 'learn, and build the future of AI-powered Flutter apps.', ) ]), - 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')]), - ]), - ]), + ], + ), ], ); } -} - -class _WhatWeDoSection extends StatelessComponent { - const _WhatWeDoSection(); - @override - Iterable build(BuildContext context) sync* { - yield section( - id: 'what-we-do', - attributes: {'role': 'region', 'aria-label': 'What We Do'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('What We Do 📦')]), - div( - classes: 'features-grid', - attributes: {'role': 'list'}, - [ - _FeatureCard( - title: 'Share Knowledge', - description: 'Technical articles, code examples, and implementation guides', - ), - _FeatureCard( - title: 'Build Tools', - description: 'Open-source packages and utilities to simplify AI integration', - ), - _FeatureCard( - title: 'Provide Templates', - description: 'Ready-to-use starter projects for common AI tasks', - ), - _FeatureCard( - title: 'Foster Collaboration', - description: 'Connect developers working on similar challenges', - ), - ], - ), - ]), + Component _buildContributorsSection() { + return ContentSection( + title: 'Want to Contribute?', + id: 'contributors', + emoji: '🔗', + children: [ + ContentCard( + children: [ + p([ + text( + '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.', + ) + ]), + p([text('PRs welcome - watch this space for contribution guidelines.')]), + ], + ), ], ); } @@ -172,51 +178,3 @@ class _FeatureCard extends StatelessComponent { ); } } - -class _JoinSection extends StatelessComponent { - const _JoinSection(); - - @override - Iterable build(BuildContext context) sync* { - yield section( - id: 'join', - attributes: {'role': 'region', 'aria-label': 'Join Us'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('Join Us')]), - p([ - text( - 'We\'re currently preparing our community platforms. Join us to collaborate, ' - 'learn, and build the future of AI-powered Flutter apps.', - ) - ]), - ]), - ], - ); - } -} - -class _ContributorsSection extends StatelessComponent { - const _ContributorsSection(); - - @override - Iterable build(BuildContext context) sync* { - yield section( - id: 'contributors', - attributes: {'role': 'region', 'aria-label': 'Want to Contribute'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('Want to Contribute? 🔗')]), - p([ - text( - '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.', - ) - ]), - p([text('PRs welcome - watch this space for contribution guidelines.')]), - ]), - ], - ); - } -} diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index e3bf025..7cf1d7a 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -1,3 +1,6 @@ +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/layouts/page_layout.dart'; import 'package:jaspr/browser.dart'; import 'package:jaspr_router/jaspr_router.dart'; @@ -19,430 +22,346 @@ class StartersPage extends StatelessComponent { Iterable build(BuildContext context) sync* { yield PageLayout( children: [ - _HeroSection(), - _CoreConceptsSection(), - _OfficialDocsSection(), - _DeepenUnderstandingSection(), - _ContributeSection(), - _ComingNextSection(), + _buildHeroSection(), + _buildCoreConceptsSection(), + _buildOfficialDocsSection(), + _buildDeepenUnderstandingSection(), + _buildContributeSection(), + _buildComingNextSection(), ], ); } -} - -class _HeroSection extends StatelessComponent { - const _HeroSection(); - @override - Iterable build(BuildContext context) sync* { - yield section( - classes: 'hero', - attributes: {'role': 'banner'}, - [ - div(classes: 'container', [ - h1( - classes: 'page-title', - [ - span( - classes: 'title-text', - [text('Starters')], - ), - ], - ), + 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( - '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.', - ) - ]), - blockquote([ - text( - '🧠 We believe in learning by doing — and starting with official sources. ' + 'We believe in learning by doing — and starting with official sources. ' 'All links below come from official documentation or deeply trusted platforms.', - ) + ), ]), - ]), - ], + ], + ), ); } -} -class _CoreConceptsSection extends StatelessComponent { - const _CoreConceptsSection(); - - @override - Iterable build(BuildContext context) sync* { - yield section( + Component _buildCoreConceptsSection() { + return ContentSection( + title: 'Core Concepts You Should Know', id: 'core-concepts', - attributes: {'role': 'region', 'aria-label': 'Core Concepts'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('🧩 Core Concepts You Should Know')]), - h3([text('What is an Agentic App?')]), - ul( - attributes: {'role': 'list'}, - [ - _ResourceItem( - emoji: '🧠', - title: 'LangChain: Introduction', - description: - 'Learn the foundations of LangChain and its modular architecture for building LLM-powered agents.', - link: 'https://python.langchain.com/docs/introduction/', - ), - _ResourceItem( - emoji: '📘', - title: 'What is Agentic AI? (IBM)', - description: - 'Overview of how agentic AI differs from traditional AI, with real-world examples.', - link: - 'https://www.ibm.com/think/topics/agentic-ai#:~:text=Agentic%20AI%20solutions%20can%20be,predictive%20analytics%20and%20execute%20trades.', - ), - _ResourceItem( - emoji: '🔍', - title: 'Introducing the Model Context Protocol (Anthropic)', - description: 'Learn how MCP standardizes interactions between LLMs and tools.', - link: 'https://www.anthropic.com/news/model-context-protocol', - ), - _ResourceItem( - emoji: '📚', - title: 'Amazon AWS: What is RAG (Retrieval-Augmented Generation)?', - description: - 'A simple primer on Retrieval-Augmented Generation and how it improves model accuracy.', - link: 'https://aws.amazon.com/what-is/retrieval-augmented-generation/', - ), - _ResourceItem( - emoji: '🎥', - title: 'Observable Flutter #59 – Agentic Apps with Flutter', - description: - 'Craig Labenz and Andrew Brogdon walk through agentic patterns, Colorist, and Firebase Genkit.', - link: - 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', - ), - ], - ), - ]), - ], - ); - } -} - -class _ResourceItem extends StatelessComponent { - const _ResourceItem({ - required this.emoji, - required this.title, - required this.description, - required this.link, - }); - - final String emoji; - final String title; - final String description; - final String link; - - @override - Iterable build(BuildContext context) sync* { - yield li( - attributes: {'role': 'listitem'}, - [ - text('$emoji '), - a( - href: link, - target: Target.blank, - attributes: { - 'aria-label': '$title - $description', - 'rel': 'noopener noreferrer', - }, - [text(title)], + emoji: '🧩', + children: [ + ul( + classes: 'resource-list', + attributes: {'role': 'list'}, + [ + ResourceLink( + emoji: '🧠', + title: 'LangChain: Introduction', + description: + 'Learn the foundations of LangChain and its modular architecture for building LLM-powered agents.', + link: 'https://python.langchain.com/docs/introduction/', + ), + ResourceLink( + emoji: '📘', + title: 'What is Agentic AI? (IBM)', + description: + 'Overview of how agentic AI differs from traditional AI, with real-world examples.', + link: + 'https://www.ibm.com/think/topics/agentic-ai#:~:text=Agentic%20AI%20solutions%20can%20be,predictive%20analytics%20and%20execute%20trades.', + ), + ResourceLink( + emoji: '🔍', + title: 'Introducing the Model Context Protocol (Anthropic)', + description: 'Learn how MCP standardizes interactions between LLMs and tools.', + link: 'https://www.anthropic.com/news/model-context-protocol', + ), + ResourceLink( + emoji: '📚', + title: 'Amazon AWS: What is RAG (Retrieval-Augmented Generation)?', + description: + 'A simple primer on Retrieval-Augmented Generation and how it improves model accuracy.', + link: 'https://aws.amazon.com/what-is/retrieval-augmented-generation/', + ), + ResourceLink( + emoji: '🎥', + title: 'Observable Flutter #59 – Agentic Apps with Flutter', + description: + 'Craig Labenz and Andrew Brogdon walk through agentic patterns, Colorist, and Firebase Genkit.', + link: + 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', + ), + ], ), - text(' – $description'), ], ); } -} -class _OfficialDocsSection extends StatelessComponent { - const _OfficialDocsSection(); - - @override - Iterable build(BuildContext context) sync* { - yield section( + Component _buildOfficialDocsSection() { + return ContentSection( + title: 'Official Docs & Tools Worth Exploring', id: 'official-docs', - attributes: {'role': 'region', 'aria-label': 'Official Documentation'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('🔗 Official Docs & Tools Worth Exploring')]), - _CategorySection( - title: 'LLM Integration (Gemini, OpenAI, Claude)', - resources: [ - _ResourceItem( - emoji: '🎨', - title: 'Gemini API + Flutter (Colorist Codelab)', - description: - 'Beginner/Intermediate. Learn how to integrate Gemini into a Flutter app using Firebase extensions and function calling.', - link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', - ), - _ResourceItem( - emoji: '🤖', - title: 'OpenAI API Docs', - description: - 'Beginner. Covers completions, chat, and function calling for GPT models.', - link: 'https://platform.openai.com/docs', - ), - _ResourceItem( - emoji: '📦', - title: 'dart_openai (Community SDK)', - description: - 'Intermediate. A Dart package that simplifies using OpenAI APIs within Flutter projects.', - link: 'https://pub.dev/packages/dart_openai', - ), - _ResourceItem( - emoji: '🧠', - title: 'Anthropic Claude API', - description: - 'Beginner. Official Claude API docs with example request/response flows.', - link: 'https://docs.anthropic.com/claude/docs', - ), - ], - ), - _CategorySection( - title: 'Speech-to-Text (Whisper)', - resources: [ - _ResourceItem( - emoji: '🎤', - title: 'OpenAI Whisper API', - description: - 'Beginner. Transcribe audio to text using OpenAI\'s hosted Whisper model.', - link: 'https://platform.openai.com/docs/guides/speech-to-text', - ), - _ResourceItem( - emoji: '📱', - title: 'flutter_whisper.cpp', - description: - 'Advanced. Community-maintained Flutter app running Whisper on-device via Rust FFI and whisper.cpp.', - link: 'https://github.com/lyledean1/flutter_whisper.cpp', - ), - ], - ), - _CategorySection( - title: 'Firebase Vector Search & RAG', - resources: [ - _ResourceItem( - emoji: '🔍', - title: 'Firestore Vector Search (Extension)', - description: - 'Intermediate. Store and retrieve vector embeddings in Firestore for building RAG pipelines.', - link: 'https://extensions.dev/extensions/googlecloud/firestore-vector-search', - ), - _ResourceItem( - emoji: '⚡', - title: 'Firebase Genkit', - description: - 'Advanced. A backend framework for building AI-driven workflows with Gemini, designed for Cloud Functions.', - link: 'https://firebase.google.com/docs/genkit', - ), - ], - ), - _CategorySection( - title: 'MCP (Model Context Protocol)', - resources: [ - _ResourceItem( - emoji: '🔄', - title: 'Model Context Protocol Overview', - description: - 'Advanced. MCP is an open protocol to connect LLMs with tools and data providers.', - link: 'https://modelcontextprotocol.io/introduction', - ), - ], - ), - _CategorySection( - title: 'Local LLMs (Ollama)', - resources: [ - _ResourceItem( - emoji: '🏠', - title: 'Ollama', - description: - 'Intermediate. Easily run LLMs like LLaMA or Mistral locally and serve them via HTTP.', - link: 'https://github.com/ollama/ollama', - ), - ], - ), - _CategorySection( - title: 'Agentic Patterns', - resources: [ - _ResourceItem( - emoji: '🔧', - title: 'Function Calling (OpenAI)', - description: - 'Intermediate. Enables models to call structured functions from your app.', - link: 'https://platform.openai.com/docs/guides/function-calling', - ), - _ResourceItem( - emoji: '🤖', - title: 'LLM Agents (Prompt Engineering Guide)', - description: 'Advanced. Explore ReAct and other reasoning/action agent patterns.', - link: 'https://www.promptingguide.ai/techniques/react', - ), - ], - ), - _CategorySection( - title: 'Codelabs, Templates, and Examples', - resources: [ - _ResourceItem( - emoji: '🎨', - title: 'Colorist (Gemini + Flutter)', - description: - 'Intermediate. Gemini-powered Flutter app that updates the UI based on natural language prompts.', - link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', - ), - _ResourceItem( - emoji: '📚', - title: 'Gemini API Cookbook (GitHub)', - description: - 'Intermediate/Advanced. Google\'s official examples for calling Gemini API across languages including Dart.', - link: 'https://github.com/google-gemini/cookbook', - ), - ], - ), - _CategorySection( - title: 'Dart AI Libraries', - resources: [ - _ResourceItem( - emoji: '🔗', - title: 'LangChain.dart', - description: - 'Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more.', - link: 'https://pub.dev/packages/langchain', - ), - ], - ), - ]), + emoji: '🔗', + children: [ + ResourceCategory( + title: 'LLM Integration (Gemini, OpenAI, Claude)', + resources: [ + ResourceLink( + emoji: '🎨', + title: 'Gemini API + Flutter (Colorist Codelab)', + description: + 'Beginner/Intermediate. Learn how to integrate Gemini into a Flutter app using Firebase extensions and function calling.', + link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', + ), + ResourceLink( + emoji: '🤖', + title: 'OpenAI API Docs', + description: + 'Beginner. Covers completions, chat, and function calling for GPT models.', + link: 'https://platform.openai.com/docs', + ), + ResourceLink( + emoji: '📦', + title: 'dart_openai (Community SDK)', + description: + 'Intermediate. A Dart package that simplifies using OpenAI APIs within Flutter projects.', + link: 'https://pub.dev/packages/dart_openai', + ), + ResourceLink( + emoji: '🧠', + title: 'Anthropic Claude API', + description: + 'Beginner. Official Claude API docs with example request/response flows.', + link: 'https://docs.anthropic.com/claude/docs', + ), + ], + ), + ResourceCategory( + title: 'Speech-to-Text (Whisper)', + resources: [ + ResourceLink( + emoji: '🎤', + title: 'OpenAI Whisper API', + description: + 'Beginner. Transcribe audio to text using OpenAI\'s hosted Whisper model.', + link: 'https://platform.openai.com/docs/guides/speech-to-text', + ), + ResourceLink( + emoji: '📱', + title: 'flutter_whisper.cpp', + description: + 'Advanced. Community-maintained Flutter app running Whisper on-device via Rust FFI and whisper.cpp.', + link: 'https://github.com/lyledean1/flutter_whisper.cpp', + ), + ], + ), + ResourceCategory( + title: 'Firebase Vector Search & RAG', + resources: [ + ResourceLink( + emoji: '🔍', + title: 'Firestore Vector Search (Extension)', + description: + 'Intermediate. Store and retrieve vector embeddings in Firestore for building RAG pipelines.', + link: 'https://extensions.dev/extensions/googlecloud/firestore-vector-search', + ), + ResourceLink( + emoji: '⚡', + title: 'Firebase Genkit', + description: + 'Advanced. A backend framework for building AI-driven workflows with Gemini, designed for Cloud Functions.', + link: 'https://firebase.google.com/docs/genkit', + ), + ], + ), + ResourceCategory( + title: 'MCP (Model Context Protocol)', + resources: [ + ResourceLink( + emoji: '🔄', + title: 'Model Context Protocol Overview', + description: + 'Advanced. MCP is an open protocol to connect LLMs with tools and data providers.', + link: 'https://modelcontextprotocol.io/introduction', + ), + ], + ), + ResourceCategory( + title: 'Local LLMs (Ollama)', + resources: [ + ResourceLink( + emoji: '🏠', + title: 'Ollama', + description: + 'Intermediate. Easily run LLMs like LLaMA or Mistral locally and serve them via HTTP.', + link: 'https://github.com/ollama/ollama', + ), + ], + ), + ResourceCategory( + title: 'Agentic Patterns', + resources: [ + ResourceLink( + emoji: '🔧', + title: 'Function Calling (OpenAI)', + description: + 'Intermediate. Enables models to call structured functions from your app.', + link: 'https://platform.openai.com/docs/guides/function-calling', + ), + ResourceLink( + emoji: '🤖', + title: 'LLM Agents (Prompt Engineering Guide)', + description: 'Advanced. Explore ReAct and other reasoning/action agent patterns.', + link: 'https://www.promptingguide.ai/techniques/react', + ), + ], + ), + ResourceCategory( + title: 'Codelabs, Templates, and Examples', + resources: [ + ResourceLink( + emoji: '🎨', + title: 'Colorist (Gemini + Flutter)', + description: + 'Intermediate. Gemini-powered Flutter app that updates the UI based on natural language prompts.', + link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', + ), + ResourceLink( + emoji: '📚', + title: 'Gemini API Cookbook (GitHub)', + description: + 'Intermediate/Advanced. Google\'s official examples for calling Gemini API across languages including Dart.', + link: 'https://github.com/google-gemini/cookbook', + ), + ], + ), + ResourceCategory( + title: 'Dart AI Libraries', + resources: [ + ResourceLink( + emoji: '🔗', + title: 'LangChain.dart', + description: + 'Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more.', + link: 'https://pub.dev/packages/langchain', + ), + ], + ), ], ); } -} - -class _CategorySection extends StatelessComponent { - const _CategorySection({ - required this.title, - required this.resources, - }); - - final String title; - final List<_ResourceItem> resources; - @override - Iterable build(BuildContext context) sync* { - yield div( - classes: 'category-section', - attributes: {'role': 'region', 'aria-label': title}, - [ - h3([text(title)]), + Component _buildDeepenUnderstandingSection() { + return ContentSection( + title: 'Want to Deepen Your Understanding?', + id: 'deepen-understanding', + emoji: '🧠', + children: [ ul( + classes: 'resource-list', attributes: {'role': 'list'}, - resources, + [ + ResourceLink( + emoji: '🎥', + title: 'Agentic Apps with Flutter (Observable Flutter #59)', + link: + 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', + description: 'Deep dive into creating agentic Flutter applications.', + ), + ResourceLink( + emoji: '💬', + title: 'Gemini Function Calling Walkthrough (Google I/O)', + link: 'https://ai.google.dev/gemini-api/docs/function-calling', + description: 'Learn how to implement function calling with Gemini.', + ), + ], ), ], ); } -} - -class _DeepenUnderstandingSection extends StatelessComponent { - const _DeepenUnderstandingSection(); - - @override - Iterable build(BuildContext context) sync* { - yield section( - id: 'deepen-understanding', - attributes: {'role': 'region', 'aria-label': 'Deepen Your Understanding'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('🧠 Want to Deepen Your Understanding?')]), - ul( - attributes: {'role': 'list'}, - [ - _ResourceItem( - emoji: '🎥', - title: 'Agentic Apps with Flutter (Observable Flutter #59)', - description: '', - link: - 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', - ), - _ResourceItem( - emoji: '💬', - title: 'Gemini Function Calling Walkthrough (Google I/O)', - description: '', - link: 'https://ai.google.dev/gemini-api/docs/function-calling', - ), - ], - ), - ]), - ], - ); - } -} - -class _ContributeSection extends StatelessComponent { - const _ContributeSection(); - @override - Iterable build(BuildContext context) sync* { - yield section( + Component _buildContributeSection() { + return ContentSection( + title: 'Contribute or Join the Circle', id: 'contribute', - attributes: {'role': 'region', 'aria-label': 'Contribute'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('🤝 Contribute or Join the Circle')]), - 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([text('👉 **Open a PR or stay tuned for our Slack/Discord — coming soon!**')]), - ]), + emoji: '🤝', + children: [ + ContentCard( + 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([text('👉 Open a PR or stay tuned for our Slack/Discord — coming soon!')]) + ], + ), ], ); } -} - -class _ComingNextSection extends StatelessComponent { - const _ComingNextSection(); - @override - Iterable build(BuildContext context) sync* { - yield section( + Component _buildComingNextSection() { + return ContentSection( + title: 'Coming Next', id: 'coming-next', - attributes: {'role': 'region', 'aria-label': 'Coming Next'}, - [ - div(classes: 'container', [ - h2(classes: 'section-title', [text('🧪 Coming Next')]), - p([ - text('Stay tuned at '), - a( - href: 'https://ai.fluttercommunity.dev', - target: Target.blank, - attributes: { - 'aria-label': 'Visit ai.fluttercommunity.dev', - 'rel': 'noopener noreferrer', - }, - [text('ai.fluttercommunity.dev')], + emoji: '🧪', + children: [ + ContentCard( + elevated: true, + children: [ + div( + classes: 'coming-next-content', + [ + p( + classes: 'coming-next-text', + [ + text('Stay tuned at '), + a( + href: 'https://ai.fluttercommunity.dev', + target: Target.blank, + attributes: { + 'aria-label': 'Visit ai.fluttercommunity.dev', + 'rel': 'noopener noreferrer', + }, + classes: 'highlight-link', + [text('ai.fluttercommunity.dev')], + ), + ], + ), + div( + classes: 'volunteer-note', + [ + span( + classes: 'emoji', + [text('🌱')], + ), + p([ + text( + 'This space is volunteer-built with ❤️ — we aim to make agentic Flutter development more accessible for everyone.', + ), + ]), + ], + ), + ], ), - ]), - p([ - text( - '🌱 *This space is volunteer-built with ❤️ — we aim to make agentic Flutter development more accessible for everyone.*', - ) - ]), - ]), + ], + ), ], ); } From 6b77c5d4c8b3bba2c846734afe4fbcfdb753aa51 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 21:04:13 +0200 Subject: [PATCH 06/31] feat: add new styles for content sections and cards * Introduce styles for content sections, cards, resource links, and emoji callouts to enhance layout and user experience. * Implement hover effects and improved spacing for better visual appeal. * Add styles for hero content and coming next sections to maintain consistency across the site. --- web/css/styles.css | 212 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/web/css/styles.css b/web/css/styles.css index 62de819..1b69563 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -504,3 +504,215 @@ button:focus-visible { border-radius: 2px; } */ + +/* Linear.app style content section */ +.content-section { + padding: var(--spacing-xl) 0; + position: relative; +} + +.content-section:after { + content: ''; + display: block; + height: 1px; + background: rgba(255, 255, 255, 0.05); + margin: 0 auto; + width: 100%; + max-width: 1200px; + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +.content-section:last-child:after { + display: none; +} + +.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 transparent; +} + +.resource-link:hover { + transform: translateY(-2px); + border-color: var(--accent-color); + box-shadow: 0 0 20px var(--shadow-color); +} + +.resource-emoji { + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; + min-width: 36px; +} + +.resource-content { + flex: 1; +} + +.resource-title { + font-weight: 600; + color: var(--accent-color); + display: block; + margin-bottom: var(--spacing-sm); +} + +.resource-description { + color: var(--secondary-text); + margin: 0; + font-size: 14px; +} + +.resource-category { + margin-bottom: var(--spacing-lg); +} + +.category-title { + font-size: 24px; + margin-bottom: var(--spacing-md); + color: var(--accent-color); + font-weight: 600; +} + +.resource-list { + list-style: none; + padding: 0; + margin: 0; +} + +/* 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; +} + +.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); +} + +.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); +} From 7b77fb3848330a579d6d209da326ab00a449de4b Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 21:25:23 +0200 Subject: [PATCH 07/31] feat: enhance ResourceLink and ResourceCategory components with new variants and styles * Introduce CardVariant and CategoryAccent enums to manage different styles for resource links and categories. * Update ResourceLink to support multiple visual variants, improving flexibility in design. * Refactor ResourceCategory to allow for grid layout and horizontal scrolling options. * Enhance CSS styles for resource links and categories, including hover effects and responsive adjustments. * Integrate new components into BuildersPage and StartersPage for improved content organization and visual appeal. --- lib/src/components/resource_link.dart | 100 ++++++++++- lib/src/pages/builders_page.dart | 74 ++++++-- lib/src/pages/starters_page.dart | 39 ++++ web/css/styles.css | 244 +++++++++++++++++++++++++- 4 files changed, 431 insertions(+), 26 deletions(-) diff --git a/lib/src/components/resource_link.dart b/lib/src/components/resource_link.dart index db4b0ce..75054b8 100644 --- a/lib/src/components/resource_link.dart +++ b/lib/src/components/resource_link.dart @@ -1,5 +1,22 @@ import 'package:jaspr/browser.dart'; +enum CardVariant { + standard, + highlighted, + compact, + blueGlow, + greenGlow, + orangeGlow, +} + +enum CategoryAccent { + none, + purple, + blue, + green, + orange, +} + class ResourceLink extends StatelessComponent { const ResourceLink({ super.key, @@ -7,20 +24,45 @@ class ResourceLink extends StatelessComponent { required this.title, required this.link, this.description, + this.variant = CardVariant.standard, }); final String emoji; final String title; final String link; final String? description; + final CardVariant variant; @override Iterable build(BuildContext context) sync* { + final variantClasses = ['resource-link']; + + switch (variant) { + case CardVariant.standard: + // No additional class needed + break; + case CardVariant.highlighted: + variantClasses.add('highlighted'); + break; + case CardVariant.compact: + variantClasses.add('compact'); + break; + case CardVariant.blueGlow: + variantClasses.add('blue-glow'); + break; + case CardVariant.greenGlow: + variantClasses.add('green-glow'); + break; + case CardVariant.orangeGlow: + variantClasses.add('orange-glow'); + break; + } + yield li( attributes: {'role': 'listitem'}, [ div( - classes: 'resource-link', + classes: variantClasses.join(' '), [ span(classes: 'resource-emoji', [text(emoji)]), div( @@ -36,7 +78,7 @@ class ResourceLink extends StatelessComponent { classes: 'resource-title', [text(title)], ), - if (description != null) + if (description != null && variant != CardVariant.compact) p( classes: 'resource-description', [text(description!)], @@ -55,23 +97,63 @@ class ResourceCategory extends StatelessComponent { super.key, required this.title, required this.resources, + this.accent = CategoryAccent.none, + this.useGrid = false, + this.useHorizontalScroll = false, }); final String title; final List resources; + final CategoryAccent accent; + final bool useGrid; + final bool useHorizontalScroll; @override Iterable build(BuildContext context) sync* { + final categoryClasses = ['resource-category']; + + switch (accent) { + case CategoryAccent.none: + // No additional class needed + break; + case CategoryAccent.purple: + categoryClasses.add('purple-accent'); + break; + case CategoryAccent.blue: + categoryClasses.add('blue-accent'); + break; + case CategoryAccent.green: + categoryClasses.add('green-accent'); + break; + case CategoryAccent.orange: + categoryClasses.add('orange-accent'); + break; + } + yield div( - classes: 'resource-category', - attributes: {'role': 'region', 'aria-label': title}, + classes: categoryClasses.join(' '), + attributes: { + 'role': 'region', + 'aria-label': title, + }, [ h3(classes: 'category-title', [text(title)]), - ul( - classes: 'resource-list', - attributes: {'role': 'list'}, - resources, - ), + if (useHorizontalScroll) + div( + classes: 'scrollable-container', + [ + div( + classes: 'scrollable-items', + resources, + ), + ], + ) + else + ul( + classes: useGrid ? 'resource-list grid-layout' : 'resource-list', + attributes: {'role': 'list'}, + resources, + ), ], ); } diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index d2943f0..8ab974f 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -1,5 +1,6 @@ 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/layouts/page_layout.dart'; import 'package:jaspr/browser.dart'; import 'package:jaspr_router/jaspr_router.dart'; @@ -45,6 +46,7 @@ class BuildersPage extends StatelessComponent { id: 'about', children: [ ContentCard( + elevated: true, children: [ p([ text( @@ -96,6 +98,7 @@ class BuildersPage extends StatelessComponent { _FeatureCard( title: 'Share Knowledge', description: 'Technical articles, code examples, and implementation guides', + isHighlighted: true, ), _FeatureCard( title: 'Build Tools', @@ -104,6 +107,7 @@ class BuildersPage extends StatelessComponent { _FeatureCard( title: 'Provide Templates', description: 'Ready-to-use starter projects for common AI tasks', + isHighlighted: true, ), _FeatureCard( title: 'Foster Collaboration', @@ -119,8 +123,10 @@ class BuildersPage extends StatelessComponent { return ContentSection( title: 'Join Us', id: 'join', + emoji: '🚀', children: [ ContentCard( + elevated: true, children: [ p([ text( @@ -128,6 +134,25 @@ class BuildersPage extends StatelessComponent { 'learn, and build the future of AI-powered Flutter apps.', ) ]), + div( + classes: 'coming-next-content', + [ + div( + classes: 'volunteer-note', + [ + span( + classes: 'emoji', + [text('🔔')], + ), + p([ + text( + 'Sign up for our newsletter to get notified when our community platforms are ready.', + ), + ]), + ], + ), + ], + ), ], ), ], @@ -140,18 +165,43 @@ class BuildersPage extends StatelessComponent { id: 'contributors', emoji: '🔗', children: [ - ContentCard( - children: [ - p([ - text( - '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.', - ) - ]), - p([text('PRs welcome - watch this space for contribution guidelines.')]), + ResourceCategory( + title: 'Ways to Contribute', + accent: CategoryAccent.blue, + useGrid: true, + resources: [ + ResourceLink( + emoji: '📝', + title: 'Write Documentation', + description: 'Help create clear guides, examples, and tutorials.', + link: '#', + variant: CardVariant.blueGlow, + ), + ResourceLink( + emoji: '🧪', + title: 'Create Example Apps', + description: 'Build sample projects demonstrating AI integration patterns.', + link: '#', + variant: CardVariant.highlighted, + ), + ResourceLink( + emoji: '🔍', + title: 'Review Code', + description: 'Help ensure quality and consistency in our code samples.', + link: '#', + ), + ResourceLink( + emoji: '🌐', + title: 'Spread the Word', + description: 'Share our resources with your network and community.', + link: '#', + ), ], ), + p( + classes: 'section-description', + [text('PRs welcome - watch this space for contribution guidelines.')], + ), ], ); } @@ -161,15 +211,17 @@ class _FeatureCard extends StatelessComponent { const _FeatureCard({ required this.title, required this.description, + this.isHighlighted = false, }); final String title; final String description; + final bool isHighlighted; @override Iterable build(BuildContext context) sync* { yield div( - classes: 'feature-card', + classes: isHighlighted ? 'feature-card highlighted' : 'feature-card', attributes: {'role': 'listitem'}, [ h3([text(title)]), diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index 7cf1d7a..563583e 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -69,6 +69,7 @@ class StartersPage extends StatelessComponent { description: 'Learn the foundations of LangChain and its modular architecture for building LLM-powered agents.', link: 'https://python.langchain.com/docs/introduction/', + variant: CardVariant.highlighted, ), ResourceLink( emoji: '📘', @@ -83,6 +84,7 @@ class StartersPage extends StatelessComponent { title: 'Introducing the Model Context Protocol (Anthropic)', description: 'Learn how MCP standardizes interactions between LLMs and tools.', link: 'https://www.anthropic.com/news/model-context-protocol', + variant: CardVariant.blueGlow, ), ResourceLink( emoji: '📚', @@ -98,6 +100,7 @@ class StartersPage extends StatelessComponent { 'Craig Labenz and Andrew Brogdon walk through agentic patterns, Colorist, and Firebase Genkit.', link: 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', + variant: CardVariant.orangeGlow, ), ], ), @@ -113,6 +116,7 @@ class StartersPage extends StatelessComponent { children: [ ResourceCategory( title: 'LLM Integration (Gemini, OpenAI, Claude)', + accent: CategoryAccent.purple, resources: [ ResourceLink( emoji: '🎨', @@ -120,6 +124,7 @@ class StartersPage extends StatelessComponent { description: 'Beginner/Intermediate. Learn how to integrate Gemini into a Flutter app using Firebase extensions and function calling.', link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', + variant: CardVariant.highlighted, ), ResourceLink( emoji: '🤖', @@ -146,6 +151,7 @@ class StartersPage extends StatelessComponent { ), ResourceCategory( title: 'Speech-to-Text (Whisper)', + accent: CategoryAccent.green, resources: [ ResourceLink( emoji: '🎤', @@ -153,6 +159,7 @@ class StartersPage extends StatelessComponent { description: 'Beginner. Transcribe audio to text using OpenAI\'s hosted Whisper model.', link: 'https://platform.openai.com/docs/guides/speech-to-text', + variant: CardVariant.greenGlow, ), ResourceLink( emoji: '📱', @@ -165,6 +172,8 @@ class StartersPage extends StatelessComponent { ), ResourceCategory( title: 'Firebase Vector Search & RAG', + accent: CategoryAccent.orange, + useGrid: true, resources: [ ResourceLink( emoji: '🔍', @@ -172,6 +181,7 @@ class StartersPage extends StatelessComponent { description: 'Intermediate. Store and retrieve vector embeddings in Firestore for building RAG pipelines.', link: 'https://extensions.dev/extensions/googlecloud/firestore-vector-search', + variant: CardVariant.orangeGlow, ), ResourceLink( emoji: '⚡', @@ -179,11 +189,13 @@ class StartersPage extends StatelessComponent { description: 'Advanced. A backend framework for building AI-driven workflows with Gemini, designed for Cloud Functions.', link: 'https://firebase.google.com/docs/genkit', + variant: CardVariant.highlighted, ), ], ), ResourceCategory( title: 'MCP (Model Context Protocol)', + accent: CategoryAccent.blue, resources: [ ResourceLink( emoji: '🔄', @@ -191,6 +203,7 @@ class StartersPage extends StatelessComponent { description: 'Advanced. MCP is an open protocol to connect LLMs with tools and data providers.', link: 'https://modelcontextprotocol.io/introduction', + variant: CardVariant.blueGlow, ), ], ), @@ -208,6 +221,8 @@ class StartersPage extends StatelessComponent { ), ResourceCategory( title: 'Agentic Patterns', + accent: CategoryAccent.purple, + useGrid: true, resources: [ ResourceLink( emoji: '🔧', @@ -221,11 +236,14 @@ class StartersPage extends StatelessComponent { title: 'LLM Agents (Prompt Engineering Guide)', description: 'Advanced. Explore ReAct and other reasoning/action agent patterns.', link: 'https://www.promptingguide.ai/techniques/react', + variant: CardVariant.highlighted, ), ], ), ResourceCategory( title: 'Codelabs, Templates, and Examples', + accent: CategoryAccent.green, + useHorizontalScroll: true, resources: [ ResourceLink( emoji: '🎨', @@ -233,6 +251,7 @@ class StartersPage extends StatelessComponent { description: 'Intermediate. Gemini-powered Flutter app that updates the UI based on natural language prompts.', link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', + variant: CardVariant.highlighted, ), ResourceLink( emoji: '📚', @@ -240,6 +259,22 @@ class StartersPage extends StatelessComponent { description: 'Intermediate/Advanced. Google\'s official examples for calling Gemini API across languages including Dart.', link: 'https://github.com/google-gemini/cookbook', + variant: CardVariant.greenGlow, + ), + ResourceLink( + emoji: '🚀', + title: 'Gemini Flutter Weather App', + description: + 'Beginner/Intermediate. Weather app powered by Gemini AI with natural language interaction.', + link: 'https://github.com/flutter/samples/tree/main/ai_weather', + ), + ResourceLink( + emoji: '📝', + title: 'OpenAI Translator Example', + description: + 'Intermediate. Demonstrates using OpenAI to build a multilingual translator app.', + link: + 'https://github.com/openai/openai-cookbook/blob/main/examples/How_to_build_a_multilingual_translator.md', ), ], ), @@ -252,6 +287,7 @@ class StartersPage extends StatelessComponent { description: 'Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more.', link: 'https://pub.dev/packages/langchain', + variant: CardVariant.highlighted, ), ], ), @@ -275,12 +311,14 @@ class StartersPage extends StatelessComponent { link: 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', description: 'Deep dive into creating agentic Flutter applications.', + variant: CardVariant.compact, ), ResourceLink( emoji: '💬', title: 'Gemini Function Calling Walkthrough (Google I/O)', link: 'https://ai.google.dev/gemini-api/docs/function-calling', description: 'Learn how to implement function calling with Gemini.', + variant: CardVariant.compact, ), ], ), @@ -295,6 +333,7 @@ class StartersPage extends StatelessComponent { emoji: '🤝', children: [ ContentCard( + elevated: true, children: [ p([ text( diff --git a/web/css/styles.css b/web/css/styles.css index 1b69563..a11072b 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -574,7 +574,6 @@ button:focus-visible { .resource-link:hover { transform: translateY(-2px); - border-color: var(--accent-color); box-shadow: 0 0 20px var(--shadow-color); } @@ -604,20 +603,246 @@ button:focus-visible { } .resource-category { - margin-bottom: var(--spacing-lg); + margin-bottom: var(--spacing-xl); + padding-top: var(--spacing-md); + position: relative; } +/* Category accent colors - using theme variables */ +.resource-category.purple-accent { + border-left: 3px solid var(--accent-color); + padding-left: var(--spacing-md); +} + +.resource-category.blue-accent { + border-left: 3px solid #4e9fff; + padding-left: var(--spacing-md); +} + +.resource-category.green-accent { + border-left: 3px solid #57ffab; + padding-left: var(--spacing-md); +} + +.resource-category.orange-accent { + border-left: 3px 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; + display: flex; + align-items: center; } -.resource-list { - list-style: none; - padding: 0; - margin: 0; +.category-title:after { + content: ''; + display: block; + height: 1px; + background: linear-gradient(90deg, var(--accent-color) 0%, transparent 100%); + flex: 1; + margin-left: var(--spacing-md); + opacity: 0.3; +} + +/* Grid layout for resource lists */ +.resource-list.grid-layout { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--spacing-md); +} + +/* Card variants - consolidating shared 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 transparent; +} + +.resource-link:hover { + transform: translateY(-2px); + box-shadow: 0 0 20px var(--shadow-color); +} + +/* Highlighted variant */ +.resource-link.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); +} + +.resource-link.highlighted:hover { + box-shadow: 0 0 20px rgba(171, 87, 255, 0.3); + border-color: rgba(171, 87, 255, 0.4); +} + +/* 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 */ +.resource-link.blue-glow { + box-shadow: 0 0 15px rgba(78, 159, 255, 0.15); + border: 1px solid rgba(78, 159, 255, 0.2); + background-color: rgba(78, 159, 255, 0.03); +} + +.resource-link.blue-glow:hover { + box-shadow: 0 0 20px rgba(78, 159, 255, 0.3); + border-color: rgba(78, 159, 255, 0.4); +} + +.resource-link.green-glow { + box-shadow: 0 0 15px rgba(87, 255, 171, 0.15); + border: 1px solid rgba(87, 255, 171, 0.2); + background-color: rgba(87, 255, 171, 0.03); +} + +.resource-link.green-glow:hover { + box-shadow: 0 0 20px rgba(87, 255, 171, 0.3); + border-color: rgba(87, 255, 171, 0.4); +} + +.resource-link.orange-glow { + box-shadow: 0 0 15px rgba(255, 159, 78, 0.15); + border: 1px solid rgba(255, 159, 78, 0.2); + background-color: rgba(255, 159, 78, 0.03); +} + +.resource-link.orange-glow:hover { + box-shadow: 0 0 20px rgba(255, 159, 78, 0.3); + border-color: rgba(255, 159, 78, 0.4); +} + +/* Scrollable container improvements */ +.scrollable-container { + overflow-x: auto; + padding-bottom: var(--spacing-md); + margin-bottom: var(--spacing-md); + -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ + scrollbar-width: thin; /* Firefox */ + scrollbar-color: var(--accent-color) transparent; /* Firefox */ + position: relative; +} + +/* Fade gradient at right edge */ +.scrollable-container:before { + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: 0; + width: 40px; + background: linear-gradient(90deg, transparent, var(--background)); + z-index: 1; + pointer-events: none; +} + +/* Only show arrow indicator when container is scrollable */ +.scrollable-container:after { + content: '→'; + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + color: var(--accent-color); + font-size: 20px; + opacity: 0.7; + z-index: 2; + pointer-events: none; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { opacity: 0.7; } + 50% { opacity: 0.3; } + 100% { opacity: 0.7; } +} + +/* 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; +} + +.scrollable-container .scrollable-items { + display: flex; + gap: var(--spacing-md); + padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm) 0; +} + +.scrollable-container .resource-link { + min-width: 280px; + max-width: 350px; + flex: 0 0 auto; +} + +/* 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; + } + + .category-title:after { + margin-left: var(--spacing-sm); + } + + .resource-link { + padding: var(--spacing-sm); + } + + .scrollable-container:after { + right: 5px; + font-size: 16px; + } } /* Emoji callout */ @@ -716,3 +941,10 @@ button:focus-visible { margin: 0; color: var(--secondary-text); } + +/* Resource lists */ +.resource-list { + list-style: none; + padding: 0; + margin: 0; +} From 7e8f960f01e30df4d0c6f9f3fc321880623a8516 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 21:57:24 +0200 Subject: [PATCH 08/31] feat: enhance content components with new properties and styles * Add elevated and animateCta properties to ContentCard for improved visual effects. * Introduce blurred property to EmojiCallout for enhanced styling options. * Update ContentSection to conditionally apply elevated styles. * Enhance ResourceCategory to support masonry grid layout and dynamic scroll buttons. * Implement JavaScript for scrollable containers to improve user interaction. * Refactor CSS for better responsiveness and visual consistency across components. --- lib/src/components/content_section.dart | 16 +- lib/src/components/resource_link.dart | 118 +++++- lib/src/pages/starters_page.dart | 28 +- web/css/styles.css | 488 ++++++++++++++++++++---- web/js/scroll.js | 77 ++++ 5 files changed, 648 insertions(+), 79 deletions(-) create mode 100644 web/js/scroll.js diff --git a/lib/src/components/content_section.dart b/lib/src/components/content_section.dart index 84013e4..de5d2c9 100644 --- a/lib/src/components/content_section.dart +++ b/lib/src/components/content_section.dart @@ -8,6 +8,7 @@ class ContentSection extends StatelessComponent { this.emoji, this.description, this.contentPadding = true, + this.elevated = false, required this.children, }); @@ -16,13 +17,14 @@ class ContentSection extends StatelessComponent { 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', + classes: ['content-section', if (elevated) 'elevated-section'].join(' '), attributes: {'role': 'region', 'aria-label': title}, [ div( @@ -56,15 +58,21 @@ class ContentCard extends StatelessComponent { 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'].join(' '), + classes: [ + 'content-card', + if (elevated) 'elevated', + if (animateCta) 'animate-cta', + ].where((c) => c.isNotEmpty).join(' '), children, ); } @@ -75,15 +83,17 @@ class EmojiCallout extends StatelessComponent { 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* { yield div( - classes: 'emoji-callout', + classes: ['emoji-callout', if (blurred) 'blurred'].join(' '), [ span(classes: 'emoji', [text(emoji)]), div(classes: 'emoji-content', children), diff --git a/lib/src/components/resource_link.dart b/lib/src/components/resource_link.dart index 75054b8..c7b817d 100644 --- a/lib/src/components/resource_link.dart +++ b/lib/src/components/resource_link.dart @@ -1,3 +1,5 @@ +import 'dart:async'; +import 'dart:html' as html; import 'package:jaspr/browser.dart'; enum CardVariant { @@ -63,6 +65,10 @@ class ResourceLink extends StatelessComponent { [ div( classes: variantClasses.join(' '), + attributes: { + // Add a data attribute for card height calculation in masonry grid + 'data-card-height': '${description != null ? (description!.length ~/ 2) + 120 : 80}', + }, [ span(classes: 'resource-emoji', [text(emoji)]), div( @@ -76,7 +82,10 @@ class ResourceLink extends StatelessComponent { 'rel': 'noopener noreferrer', }, classes: 'resource-title', - [text(title)], + [ + text(title), + // The arrow is now added via CSS :after pseudo-element + ], ), if (description != null && variant != CardVariant.compact) p( @@ -100,6 +109,7 @@ class ResourceCategory extends StatelessComponent { this.accent = CategoryAccent.none, this.useGrid = false, this.useHorizontalScroll = false, + this.useMasonryGrid = false, }); final String title; @@ -107,10 +117,12 @@ class ResourceCategory extends StatelessComponent { final CategoryAccent accent; final bool useGrid; final bool useHorizontalScroll; + final bool useMasonryGrid; @override Iterable build(BuildContext context) sync* { final categoryClasses = ['resource-category']; + final String containerId = 'scrollable-${DateTime.now().millisecondsSinceEpoch}'; switch (accent) { case CategoryAccent.none: @@ -140,8 +152,64 @@ class ResourceCategory extends StatelessComponent { h3(classes: 'category-title', [text(title)]), if (useHorizontalScroll) div( - classes: 'scrollable-container', + classes: 'scrollable-container dynamic-scroll-buttons', + attributes: { + 'id': containerId, + // Add the check overflow logic as an onMount script attribute + 'data-onrender': ''' + const container = document.getElementById('$containerId'); + if (container) { + const items = container.querySelector('.scrollable-items'); + const leftBtn = container.querySelector('.scroll-nav-left'); + const rightBtn = container.querySelector('.scroll-nav-right'); + + function checkOverflow() { + if (items && leftBtn && rightBtn) { + const hasOverflow = items.scrollWidth > items.clientWidth; + leftBtn.style.display = hasOverflow ? 'flex' : 'none'; + rightBtn.style.display = hasOverflow ? 'flex' : 'none'; + } + } + + // Check on load + checkOverflow(); + + // Check on resize + window.addEventListener('resize', checkOverflow); + + // Add event listeners for button clicks + if (leftBtn) { + leftBtn.addEventListener('click', () => { + items.scrollBy({ left: -300, behavior: 'smooth' }); + }); + } + + if (rightBtn) { + rightBtn.addEventListener('click', () => { + items.scrollBy({ left: 300, behavior: 'smooth' }); + }); + } + } + ''' + }, [ + // Navigation buttons (visibility controlled by JS) + div( + classes: 'scroll-nav-button scroll-nav-left', + attributes: { + 'aria-label': 'Scroll left', + 'style': 'display: none;', // Hidden by default + }, + [text('←')], + ), + div( + classes: 'scroll-nav-button scroll-nav-right', + attributes: { + 'aria-label': 'Scroll right', + 'style': 'display: none;', // Hidden by default + }, + [text('→')], + ), div( classes: 'scrollable-items', resources, @@ -150,11 +218,53 @@ class ResourceCategory extends StatelessComponent { ) else ul( - classes: useGrid ? 'resource-list grid-layout' : 'resource-list', - attributes: {'role': 'list'}, + classes: [ + 'resource-list', + if (useGrid) 'grid-layout', + if (useMasonryGrid) 'masonry-grid', + ].where((c) => c.isNotEmpty).join(' '), + attributes: { + 'role': 'list', + if (useMasonryGrid) + 'data-onrender': ''' + document.querySelectorAll('.masonry-grid .resource-link').forEach(card => { + const height = card.getAttribute('data-card-height'); + if (height) { + card.style.setProperty('--card-height', height); + } + }); + ''', + }, resources, ), ], ); + + // If we're using horizontal scroll or masonry grid, also add a script to the page + // to look for the data-onrender attribute and execute it + if (useHorizontalScroll || useMasonryGrid) { + yield script( + src: '', + attributes: {'type': 'text/javascript'}, + [ + text(''' + document.addEventListener('DOMContentLoaded', function() { + // Find all elements with data-onrender attribute + document.querySelectorAll('[data-onrender]').forEach(function(el) { + // Execute the script in data-onrender + try { + const scriptContent = el.getAttribute('data-onrender'); + if (scriptContent) { + new Function(scriptContent)(); + } + } catch (e) { + console.error('Error executing onrender script:', e); + } + }); + }); + '''), + ], + ); + } } } diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index 563583e..e7a0754 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -280,6 +280,7 @@ class StartersPage extends StatelessComponent { ), ResourceCategory( title: 'Dart AI Libraries', + useMasonryGrid: true, resources: [ ResourceLink( emoji: '🔗', @@ -289,6 +290,21 @@ class StartersPage extends StatelessComponent { link: 'https://pub.dev/packages/langchain', variant: CardVariant.highlighted, ), + ResourceLink( + emoji: '🤖', + title: 'Flutter GenAI', + description: + 'Advanced. A collection of Flutter plugins for integrating generative AI capabilities into your applications.', + link: 'https://pub.dev/packages/langchain', + variant: CardVariant.blueGlow, + ), + ResourceLink( + emoji: '📱', + title: 'Dart OpenAI', + description: + 'Beginner/Intermediate. A Dart client for OpenAI API with chat, completions, embeddings and more.', + link: 'https://pub.dev/packages/dart_openai', + ), ], ), ], @@ -331,9 +347,11 @@ class StartersPage extends StatelessComponent { title: 'Contribute or Join the Circle', id: 'contribute', emoji: '🤝', + elevated: true, children: [ ContentCard( elevated: true, + animateCta: true, children: [ p([ text( @@ -348,7 +366,14 @@ class StartersPage extends StatelessComponent { li([text('Help write docs or translate examples')]), ], ), - p([text('👉 Open a PR or stay tuned for our Slack/Discord — coming soon!')]) + p([ + span( + classes: 'cta_button', + [ + span([text('👉 Open a PR or stay tuned for our Slack/Discord — coming soon!')]) + ], + ) + ]) ], ), ], @@ -360,6 +385,7 @@ class StartersPage extends StatelessComponent { title: 'Coming Next', id: 'coming-next', emoji: '🧪', + elevated: true, children: [ ContentCard( elevated: true, diff --git a/web/css/styles.css b/web/css/styles.css index a11072b..ba2add9 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -114,17 +114,41 @@ 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); @@ -487,6 +511,9 @@ button:focus-visible { 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 */ @@ -507,26 +534,19 @@ button:focus-visible { /* Linear.app style content section */ .content-section { - padding: var(--spacing-xl) 0; + padding: 64px 0; position: relative; } -.content-section:after { +.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; - height: 1px; - background: rgba(255, 255, 255, 0.05); - margin: 0 auto; - width: 100%; - max-width: 1200px; - position: absolute; - bottom: 0; - left: 0; - right: 0; -} - -.content-section:last-child:after { - display: none; } .section-content { @@ -568,21 +588,25 @@ button:focus-visible { background-color: var(--card-bg); border-radius: var(--border-radius); margin-bottom: var(--spacing-md); - transition: all var(--transition-speed); + transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); border: 1px solid transparent; } .resource-link:hover { - transform: translateY(-2px); - box-shadow: 0 0 20px var(--shadow-color); + transform: translateY(-5px); + box-shadow: 0 8px 24px var(--shadow-color); } .resource-emoji { - font-size: 24px; + 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 { @@ -592,8 +616,22 @@ button:focus-visible { .resource-title { font-weight: 600; color: var(--accent-color); - display: block; - margin-bottom: var(--spacing-sm); + display: flex; + align-items: center; + gap: 8px; +} + +.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 { @@ -603,7 +641,7 @@ button:focus-visible { } .resource-category { - margin-bottom: var(--spacing-xl); + margin-bottom: 48px; padding-top: var(--spacing-md); position: relative; } @@ -639,6 +677,16 @@ button:focus-visible { align-items: center; } +.category-title:before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--accent-color); + margin-right: 12px; +} + .category-title:after { content: ''; display: block; @@ -734,76 +782,169 @@ button:focus-visible { border-color: rgba(255, 159, 78, 0.4); } -/* Scrollable container improvements */ +/* Scrollable container styles */ .scrollable-container { - overflow-x: auto; - padding-bottom: var(--spacing-md); - margin-bottom: var(--spacing-md); - -webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */ - scrollbar-width: thin; /* Firefox */ - scrollbar-color: var(--accent-color) transparent; /* Firefox */ position: relative; + margin-bottom: var(--spacing-md); } -/* Fade gradient at right edge */ -.scrollable-container:before { - content: ''; - position: absolute; - right: 0; - top: 0; - bottom: 0; - width: 40px; - background: linear-gradient(90deg, transparent, var(--background)); - z-index: 1; - pointer-events: none; +/* Only enable scrolling on mobile */ +@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; + } + + /* Fade gradient at right edge */ + .scrollable-container:before { + content: ''; + position: absolute; + right: 0; + top: 0; + bottom: var(--spacing-md); + width: 40px; + background: linear-gradient(90deg, transparent, var(--background)); + z-index: 1; + pointer-events: none; + } + + /* Arrow indicator for scroll */ + .scrollable-container:after { + content: '→'; + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + color: var(--accent-color); + font-size: 20px; + opacity: 0.7; + z-index: 2; + pointer-events: none; + animation: pulse-hint 2s infinite ease-in-out; + } + + /* 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; + } } -/* Only show arrow indicator when container is scrollable */ -.scrollable-container:after { - content: '→'; - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - color: var(--accent-color); - font-size: 20px; - opacity: 0.7; - z-index: 2; - pointer-events: none; - animation: pulse 2s infinite; +/* 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; + } +} + +/* 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; } } -/* Scrollbar styling */ -.scrollable-container::-webkit-scrollbar { - height: 6px; +/* 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::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 10px; +.scrollable-container:hover .scroll-nav-button { + opacity: 0.8; } -.scrollable-container::-webkit-scrollbar-thumb { - background: var(--accent-color); - border-radius: 10px; +.scroll-nav-button:hover { + background-color: rgba(171, 87, 255, 0.3); + opacity: 1; } -.scrollable-container .scrollable-items { - display: flex; - gap: var(--spacing-md); - padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-sm) 0; +.scroll-nav-left { + left: -8px; +} + +.scroll-nav-right { + right: -8px; +} + +/* Default cursor styles for scrollable areas */ +.scrollable-items { + cursor: grab; } -.scrollable-container .resource-link { - min-width: 280px; - max-width: 350px; - flex: 0 0 auto; +.scrollable-items:active { + cursor: grabbing; } /* Feature card improvements */ @@ -853,6 +994,9 @@ button:focus-visible { 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 { @@ -889,6 +1033,18 @@ button:focus-visible { display: flex; flex-direction: column; gap: var(--spacing-md); + position: relative; +} + +.coming-next-content:before { + content: ''; + position: absolute; + left: 0; + top: 50%; + width: 100%; + height: 2px; + background: linear-gradient(90deg, var(--accent-color), transparent); + opacity: 0.2; } .coming-next-text { @@ -948,3 +1104,193 @@ button:focus-visible { padding: 0; margin: 0; } + +/* Alternating category backgrounds */ +.resource-category:nth-child(odd) { + background-color: rgba(255, 255, 255, 0.01); +} + +/* 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; +} + +.coming-next-content:before { + content: ''; + position: absolute; + left: 0; + top: 50%; + width: 100%; + height: 2px; + background: linear-gradient(90deg, var(--accent-color), transparent); + opacity: 0.2; +} + +/* 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; +} 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 From 6292ef389a278f926d7411abe6567ac5b055c57c Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 22:02:10 +0200 Subject: [PATCH 09/31] feat: add Builder's Toolkit section and enhance feature card presentation * Introduce a new Builder's Toolkit section in BuildersPage to provide resources and tools for Flutter developers. * Enhance feature cards with optional emoji support for improved visual engagement. * Update CSS to style feature card emojis for better alignment and presentation. * Refactor existing sections to maintain consistency and improve overall layout. --- lib/src/pages/builders_page.dart | 126 +++++++++++++++++++++++++++++-- web/css/styles.css | 10 +++ 2 files changed, 131 insertions(+), 5 deletions(-) diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index 8ab974f..9a0145a 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -26,6 +26,7 @@ class BuildersPage extends StatelessComponent { _buildAboutSection(), _buildMissionSection(), _buildWhatWeDoSection(), + _buildToolkitSection(), _buildJoinSection(), _buildContributorsSection(), ], @@ -37,6 +38,16 @@ class BuildersPage extends StatelessComponent { 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.', + ), + ]), + ], + ), ); } @@ -44,6 +55,8 @@ class BuildersPage extends StatelessComponent { return ContentSection( title: 'About Us', id: 'about', + emoji: '🌟', + description: 'We connect Flutter developers building with modern AI capabilities.', children: [ ContentCard( elevated: true, @@ -99,19 +112,89 @@ class BuildersPage extends StatelessComponent { 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: '🤝', + ), + ], + ), + ], + ); + } + + 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 Tools', + accent: CategoryAccent.purple, + useGrid: true, + resources: [ + ResourceLink( + emoji: '🔄', + title: 'Function Calling Patterns', + description: 'Reference implementations of function calling with Gemini and Claude.', + link: '#', + variant: CardVariant.highlighted, + ), + ResourceLink( + emoji: '🔍', + title: 'RAG Implementation Guide', + description: 'Step-by-step guide to implementing retrieval-augmented generation.', + link: '#', + variant: CardVariant.blueGlow, + ), + ], + ), + ResourceCategory( + title: 'Advanced Workflows', + accent: CategoryAccent.green, + useHorizontalScroll: true, + resources: [ + ResourceLink( + emoji: '🧠', + title: 'Agent Orchestration', + description: 'Patterns for coordinating multiple AI agents in a Flutter app.', + link: '#', + variant: CardVariant.highlighted, + ), + ResourceLink( + emoji: '📱', + title: 'On-Device LLM Integration', + description: 'Guide to running smaller language models locally in Flutter.', + link: '#', + variant: CardVariant.greenGlow, + ), + ResourceLink( + emoji: '🔊', + title: 'Voice Input Processing', + description: 'Integrating speech recognition with LLM processing pipelines.', + link: '#', + ), + ResourceLink( + emoji: '📷', + title: 'Vision + LLM', + description: 'Combining image processing with language model understanding.', + link: '#', + variant: CardVariant.orangeGlow, ), ], ), @@ -123,10 +206,12 @@ class BuildersPage extends StatelessComponent { return ContentSection( title: 'Join Us', id: 'join', - emoji: '🚀', + emoji: '✨', + elevated: true, children: [ ContentCard( elevated: true, + animateCta: true, children: [ p([ text( @@ -151,6 +236,18 @@ class BuildersPage extends StatelessComponent { ]), ], ), + div( + classes: 'buttons-container', + [ + a( + href: '#', + classes: 'cta_button', + [ + span([text('Join the Community')]), + ], + ), + ], + ), ], ), ], @@ -164,6 +261,7 @@ class BuildersPage extends StatelessComponent { title: 'Want to Contribute?', id: 'contributors', emoji: '🔗', + elevated: true, children: [ ResourceCategory( title: 'Ways to Contribute', @@ -198,9 +296,22 @@ class BuildersPage extends StatelessComponent { ), ], ), - p( - classes: 'section-description', - [text('PRs welcome - watch this space for contribution guidelines.')], + ContentCard( + children: [ + p( + classes: 'section-description', + [text('PRs welcome - watch this space for contribution guidelines.')], + ), + p([ + text('Check out our '), + a( + href: '/starters', + classes: 'highlight-link', + [text('Starters')], + ), + text(' section for inspiration and examples to build upon.'), + ]), + ], ), ], ); @@ -212,11 +323,13 @@ class _FeatureCard extends StatelessComponent { required this.title, required this.description, this.isHighlighted = false, + this.emoji, }); final String title; final String description; final bool isHighlighted; + final String? emoji; @override Iterable build(BuildContext context) sync* { @@ -224,7 +337,10 @@ class _FeatureCard extends StatelessComponent { classes: isHighlighted ? 'feature-card highlighted' : 'feature-card', attributes: {'role': 'listitem'}, [ - h3([text(title)]), + h3([ + if (emoji != null) span(classes: 'feature-emoji', [text('$emoji ')]), + text(title), + ]), p([text(description)]), ], ); diff --git a/web/css/styles.css b/web/css/styles.css index ba2add9..8c914b7 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -1294,3 +1294,13 @@ button:focus-visible { 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); +} From 341dbce5b8388218c3454f96e961819daebfe064 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 22:19:17 +0200 Subject: [PATCH 10/31] feat: update resource links and enhance call-to-action button * Change the link for the Flutter GenAI resource to the correct package URL. * Replace the text in the call-to-action button with a link to contribute on GitHub, improving user engagement. * Update the coming next text to include a more inviting message for users to stay tuned for additional resources. --- lib/src/pages/starters_page.dart | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index e7a0754..51b4c0c 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -295,7 +295,7 @@ class StartersPage extends StatelessComponent { title: 'Flutter GenAI', description: 'Advanced. A collection of Flutter plugins for integrating generative AI capabilities into your applications.', - link: 'https://pub.dev/packages/langchain', + link: 'https://pub.dev/packages/flutter_genai', variant: CardVariant.blueGlow, ), ResourceLink( @@ -370,7 +370,15 @@ class StartersPage extends StatelessComponent { span( classes: 'cta_button', [ - span([text('👉 Open a PR or stay tuned for our Slack/Discord — coming soon!')]) + a( + href: 'https://github.com/fluttercommunity/fc_ai_circle', + target: Target.blank, + attributes: { + 'aria-label': 'Contribute on GitHub', + 'rel': 'noopener noreferrer', + }, + [text('👉 Open a PR on GitHub')], + ) ], ) ]) @@ -396,7 +404,7 @@ class StartersPage extends StatelessComponent { p( classes: 'coming-next-text', [ - text('Stay tuned at '), + text('✨ Stay tuned for more resources at '), a( href: 'https://ai.fluttercommunity.dev', target: Target.blank, From bac0ab7592fdd9e4ac13a35473d6229896307090 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 22:19:28 +0200 Subject: [PATCH 11/31] feat: add TODO for future link in HeroSection * Add a TODO comment to the HeroSection for linking to the "What is Agentic Flutter?" page once it is created, enhancing future content planning. --- lib/src/pages/home_page.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home_page.dart index 1390ff1..9e3a599 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/pages/home_page.dart @@ -67,6 +67,7 @@ class _HeroSection extends StatelessComponent { a( classes: 'secondary-button', href: '#', + // TODO: Add link to "What is Agentic Flutter?" page once created [text('What is Agentic Flutter?')], ), ]), From e22c3a8ac312d11060cfb02d2542644ade92e0ea Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sat, 10 May 2025 22:19:40 +0200 Subject: [PATCH 12/31] feat: enhance accessibility and improve emoji handling in components * Update ContentSection and EmojiCallout to include aria attributes for better screen reader support. * Add a helper method to provide descriptions for common emojis, enhancing accessibility for visually impaired users. * Improve styling for resource links and scroll buttons in ResourceCategory for better contrast and usability. * Add TODO comments in BuildersPage for future documentation links, aiding in content planning. --- lib/src/components/content_section.dart | 49 ++++++++++++++++++++++++- lib/src/components/resource_link.dart | 10 +++-- lib/src/pages/builders_page.dart | 11 ++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/lib/src/components/content_section.dart b/lib/src/components/content_section.dart index de5d2c9..51b94e7 100644 --- a/lib/src/components/content_section.dart +++ b/lib/src/components/content_section.dart @@ -33,7 +33,11 @@ class ContentSection extends StatelessComponent { h2( classes: 'section-title', [ - if (emoji != null) text('$emoji '), + if (emoji != null) + span( + attributes: {'aria-hidden': 'true'}, + [text('$emoji ')], + ), text(title), ], ), @@ -92,12 +96,53 @@ class EmojiCallout extends StatelessComponent { @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', [text(emoji)]), + 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/resource_link.dart b/lib/src/components/resource_link.dart index c7b817d..a749f8d 100644 --- a/lib/src/components/resource_link.dart +++ b/lib/src/components/resource_link.dart @@ -70,7 +70,7 @@ class ResourceLink extends StatelessComponent { 'data-card-height': '${description != null ? (description!.length ~/ 2) + 120 : 80}', }, [ - span(classes: 'resource-emoji', [text(emoji)]), + span(classes: 'resource-emoji', attributes: {'aria-hidden': 'true'}, [text(emoji)]), div( classes: 'resource-content', [ @@ -198,7 +198,9 @@ class ResourceCategory extends StatelessComponent { classes: 'scroll-nav-button scroll-nav-left', attributes: { 'aria-label': 'Scroll left', - 'style': 'display: none;', // Hidden by default + 'style': + 'display: none; background-color: var(--primary-color); color: white;', // Hidden by default, improved contrast + 'title': 'Scroll to see previous items', }, [text('←')], ), @@ -206,7 +208,9 @@ class ResourceCategory extends StatelessComponent { classes: 'scroll-nav-button scroll-nav-right', attributes: { 'aria-label': 'Scroll right', - 'style': 'display: none;', // Hidden by default + 'style': + 'display: none; background-color: var(--primary-color); color: white;', // Hidden by default, improved contrast + 'title': 'Scroll to see more items', }, [text('→')], ), diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index 9a0145a..bae622a 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -153,6 +153,7 @@ class BuildersPage extends StatelessComponent { title: 'Function Calling Patterns', description: 'Reference implementations of function calling with Gemini and Claude.', link: '#', + // TODO: Add link to function calling patterns documentation once available variant: CardVariant.highlighted, ), ResourceLink( @@ -160,6 +161,7 @@ class BuildersPage extends StatelessComponent { title: 'RAG Implementation Guide', description: 'Step-by-step guide to implementing retrieval-augmented generation.', link: '#', + // TODO: Add link to RAG implementation guide once published variant: CardVariant.blueGlow, ), ], @@ -174,6 +176,7 @@ class BuildersPage extends StatelessComponent { title: 'Agent Orchestration', description: 'Patterns for coordinating multiple AI agents in a Flutter app.', link: '#', + // TODO: Add link to agent orchestration documentation once available variant: CardVariant.highlighted, ), ResourceLink( @@ -181,6 +184,7 @@ class BuildersPage extends StatelessComponent { title: 'On-Device LLM Integration', description: 'Guide to running smaller language models locally in Flutter.', link: '#', + // TODO: Add link to on-device LLM integration guide once published variant: CardVariant.greenGlow, ), ResourceLink( @@ -188,12 +192,14 @@ class BuildersPage extends StatelessComponent { title: 'Voice Input Processing', description: 'Integrating speech recognition with LLM processing pipelines.', link: '#', + // TODO: Add link to voice processing tutorial once available ), ResourceLink( emoji: '📷', title: 'Vision + LLM', description: 'Combining image processing with language model understanding.', link: '#', + // TODO: Add link to vision+LLM integration guide once published variant: CardVariant.orangeGlow, ), ], @@ -241,6 +247,7 @@ class BuildersPage extends StatelessComponent { [ a( href: '#', + // TODO: Add link to community signup form once available classes: 'cta_button', [ span([text('Join the Community')]), @@ -273,6 +280,7 @@ class BuildersPage extends StatelessComponent { title: 'Write Documentation', description: 'Help create clear guides, examples, and tutorials.', link: '#', + // TODO: Add link to documentation contribution guidelines once available variant: CardVariant.blueGlow, ), ResourceLink( @@ -280,6 +288,7 @@ class BuildersPage extends StatelessComponent { title: 'Create Example Apps', description: 'Build sample projects demonstrating AI integration patterns.', link: '#', + // TODO: Add link to example app contribution guidelines once available variant: CardVariant.highlighted, ), ResourceLink( @@ -287,12 +296,14 @@ class BuildersPage extends StatelessComponent { title: 'Review Code', description: 'Help ensure quality and consistency in our code samples.', link: '#', + // TODO: Add link to code review guidelines once published ), ResourceLink( emoji: '🌐', title: 'Spread the Word', description: 'Share our resources with your network and community.', link: '#', + // TODO: Add link to community outreach resources once available ), ], ), From 7177e04a1519b9453db646a1d2a501a753468fce Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 18:56:36 +0200 Subject: [PATCH 13/31] refactor: comment out unused link in HeroSection for future content planning * Comment out the link to the "What is Agentic Flutter?" page in the HeroSection, maintaining the TODO for future implementation once the page is created. This enhances clarity in the code and aids in content planning. --- lib/src/pages/home_page.dart | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home_page.dart index 9e3a599..fa5af38 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/pages/home_page.dart @@ -64,12 +64,13 @@ class _HeroSection extends StatelessComponent { onClick: () => Router.of(context).push(BuildersPage.path), [text('Explore Builders')], ), - a( - classes: 'secondary-button', - href: '#', - // TODO: Add link to "What is Agentic Flutter?" page once created - [text('What is Agentic Flutter?')], - ), + // TODO: Add link to "What is Agentic Flutter?" page once created + // a( + // classes: 'secondary-button', + // href: '#', + // // TODO: Add link to "What is Agentic Flutter?" page once created + // [text('What is Agentic Flutter?')], + // ), ]), ]), ]); From 15ed057a4d7b7d28349619d934203edda4534dcb Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 19:14:43 +0200 Subject: [PATCH 14/31] feat: add new external links and enhance footer component * Introduce a new YouTube link for Agentic QA in the ExternalLink enum. * Update the Footer component to utilize the new YouTube link and survey link, improving resource accessibility. * Modify the link rendering logic in FooterColumn to handle external links with appropriate attributes for better accessibility and security. * Update the HomePage to include the new YouTube link for "What is Agentic Flutter?" and adjust feature cards to reference the correct URLs, enhancing user engagement. --- lib/src/app/external_links.dart | 1 + lib/src/components/footer.dart | 25 ++++++++++---- lib/src/pages/home_page.dart | 60 ++++++++++++++++++++++----------- 3 files changed, 59 insertions(+), 27 deletions(-) diff --git a/lib/src/app/external_links.dart b/lib/src/app/external_links.dart index e11e24e..3aa3f29 100644 --- a/lib/src/app/external_links.dart +++ b/lib/src/app/external_links.dart @@ -1,6 +1,7 @@ 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'), 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/components/footer.dart b/lib/src/components/footer.dart index 608f8dd..da9c850 100644 --- a/lib/src/components/footer.dart +++ b/lib/src/components/footer.dart @@ -13,8 +13,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( @@ -88,11 +88,22 @@ 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.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: () => Router.of(context).push(link.path), + [text(link.label)], + ) ]), ]), ]); diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home_page.dart index fa5af38..ae660a5 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'; @@ -64,13 +65,16 @@ class _HeroSection extends StatelessComponent { onClick: () => Router.of(context).push(BuildersPage.path), [text('Explore Builders')], ), - // TODO: Add link to "What is Agentic Flutter?" page once created - // a( - // classes: 'secondary-button', - // href: '#', - // // TODO: Add link to "What is Agentic Flutter?" page once created - // [text('What is Agentic Flutter?')], - // ), + a( + classes: 'secondary-button', + href: ExternalLink.youTubeAgenticQA.url, + target: Target.blank, + attributes: { + 'rel': 'noopener noreferrer', + 'aria-label': 'Learn what Agentic Flutter is', + }, + [text('What is Agentic Flutter?')], + ), ]), ]), ]); @@ -90,21 +94,20 @@ 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', + url: ExternalLink.youTubeVibeCoding.url, ), - // 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, ), ]), ]), @@ -116,20 +119,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, + ); + } } } From 90e2bbb4305d1855921853b17f94e01f22ae5fea Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 19:33:14 +0200 Subject: [PATCH 15/31] fix: update link attributes in HeroSection and enhance button hover style * Add comments to clarify the purpose of the YouTube link in the HeroSection, improving code readability. * Update CSS to include a pointer cursor on hover for the secondary button, enhancing user interaction and visual feedback. * Comment out unused CSS for a previously animated gradient, streamlining the styles for better performance. --- lib/src/pages/home_page.dart | 4 ++-- web/css/styles.css | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/src/pages/home_page.dart b/lib/src/pages/home_page.dart index ae660a5..61f3408 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/pages/home_page.dart @@ -67,8 +67,8 @@ class _HeroSection extends StatelessComponent { ), a( classes: 'secondary-button', - href: ExternalLink.youTubeAgenticQA.url, - target: Target.blank, + 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', diff --git a/web/css/styles.css b/web/css/styles.css index 8c914b7..9f57e6a 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -92,6 +92,7 @@ a:hover { color: var(--text-color); font-weight: 600; transition: all var(--transition-speed); + cursor: pointer; } .secondary-button:hover { @@ -120,6 +121,7 @@ a:hover { } /* Replace the complex animated gradient with a simple, static subtle spotlight */ +/* .hero:before { content: ''; position: absolute; @@ -134,7 +136,9 @@ a:hover { ); /* Remove animation that causes visual noise */ /* animation: pulse-bg 10s infinite alternate ease-in-out; */ +/* } +*/ /* Remove the distracting animation */ /* @keyframes pulse-bg { From be74bbafb13940600de6b6942ab18e377f4e151f Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 19:45:52 +0200 Subject: [PATCH 16/31] feat: update resource links in StartersPage for improved content accessibility * Remove outdated dart_openai link and replace it with mcp_dart, enhancing the variety of SDKs available for users. * Reintroduce the dart_openai link with updated description, ensuring users have access to relevant resources for OpenAI APIs. * Streamline resource links to provide a more comprehensive set of tools for developers. --- lib/src/pages/starters_page.dart | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index 51b4c0c..5bba617 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -133,13 +133,6 @@ class StartersPage extends StatelessComponent { 'Beginner. Covers completions, chat, and function calling for GPT models.', link: 'https://platform.openai.com/docs', ), - ResourceLink( - emoji: '📦', - title: 'dart_openai (Community SDK)', - description: - 'Intermediate. A Dart package that simplifies using OpenAI APIs within Flutter projects.', - link: 'https://pub.dev/packages/dart_openai', - ), ResourceLink( emoji: '🧠', title: 'Anthropic Claude API', @@ -147,6 +140,20 @@ class StartersPage extends StatelessComponent { 'Beginner. Official Claude API docs with example request/response flows.', link: 'https://docs.anthropic.com/claude/docs', ), + ResourceLink( + emoji: '📦', + title: 'mcp_dart (Community SDK)', + description: + 'Integrate LLM applications with external data and tools using this Dart SDK for the Model Context Protocol (MCP).', + link: 'https://pub.dev/packages/mcp_dart', + ), + ResourceLink( + emoji: '📦', + title: 'dart_openai (Community SDK)', + description: + 'Intermediate. A Dart package that simplifies using OpenAI APIs within Flutter projects.', + link: 'https://pub.dev/packages/dart_openai', + ), ], ), ResourceCategory( From 4cb5bb4536872730698a75d93f577db93be43e4e Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 20:03:38 +0200 Subject: [PATCH 17/31] feat: update StartersPage with new external link and improve button accessibility * Add external link for the Contributors Survey, enhancing user engagement and resource accessibility. * Remove masonry grid usage in ResourceCategory for a cleaner layout. * Update button text for clarity, improving call-to-action effectiveness. * Adjust CSS for button styles to ensure consistent height and box-sizing, enhancing visual presentation. --- lib/src/pages/starters_page.dart | 8 ++++---- web/css/styles.css | 10 +++++++--- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index 5bba617..c66b17b 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -2,6 +2,7 @@ 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/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'; @@ -287,7 +288,6 @@ class StartersPage extends StatelessComponent { ), ResourceCategory( title: 'Dart AI Libraries', - useMasonryGrid: true, resources: [ ResourceLink( emoji: '🔗', @@ -378,13 +378,13 @@ class StartersPage extends StatelessComponent { classes: 'cta_button', [ a( - href: 'https://github.com/fluttercommunity/fc_ai_circle', + href: ExternalLink.surveyContributors.url, target: Target.blank, attributes: { - 'aria-label': 'Contribute on GitHub', + 'aria-label': 'Contribute: Take the Contributors Survey', 'rel': 'noopener noreferrer', }, - [text('👉 Open a PR on GitHub')], + [text('Become a Contributor')], ) ], ) diff --git a/web/css/styles.css b/web/css/styles.css index 9f57e6a..8f809b8 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -592,13 +592,15 @@ button:focus-visible { background-color: var(--card-bg); border-radius: var(--border-radius); margin-bottom: var(--spacing-md); - transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1); + transition: all var(--transition-speed); border: 1px solid transparent; + height: 100%; + box-sizing: border-box; } .resource-link:hover { - transform: translateY(-5px); - box-shadow: 0 8px 24px var(--shadow-color); + transform: translateY(-2px); + box-shadow: 0 0 20px var(--shadow-color); } .resource-emoji { @@ -718,6 +720,8 @@ button:focus-visible { margin-bottom: var(--spacing-md); transition: all var(--transition-speed); border: 1px solid transparent; + height: 100%; + box-sizing: border-box; } .resource-link:hover { From 75920ab001d9b5b73c726695daa67568c03f0a05 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 20:15:50 +0200 Subject: [PATCH 18/31] feat: add new YouTube link and enhance Coming Next section in StartersPage * Introduce a new YouTube link for the Flutter Community channel in the ExternalLink enum, improving resource accessibility. * Update the Coming Next section title and emoji for better engagement. * Revise button texts to clarify actions and enhance user interaction. * Adjust CSS for buttons to ensure consistent layout and spacing, improving visual presentation. --- lib/src/app/external_links.dart | 1 + lib/src/pages/starters_page.dart | 79 ++++++++++++++++++-------------- web/css/styles.css | 7 ++- 3 files changed, 50 insertions(+), 37 deletions(-) diff --git a/lib/src/app/external_links.dart b/lib/src/app/external_links.dart index 3aa3f29..fda3c5d 100644 --- a/lib/src/app/external_links.dart +++ b/lib/src/app/external_links.dart @@ -2,6 +2,7 @@ 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'), 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/pages/starters_page.dart b/lib/src/pages/starters_page.dart index c66b17b..17358ed 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -397,49 +397,58 @@ class StartersPage extends StatelessComponent { Component _buildComingNextSection() { return ContentSection( - title: 'Coming Next', + title: 'Stay Connected & See What\'s Next', id: 'coming-next', - emoji: '🧪', + emoji: '📺', elevated: true, children: [ ContentCard( elevated: true, children: [ - div( - classes: 'coming-next-content', - [ - p( - classes: 'coming-next-text', + 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('✨ Stay tuned for more resources at '), - a( - href: 'https://ai.fluttercommunity.dev', - target: Target.blank, - attributes: { - 'aria-label': 'Visit ai.fluttercommunity.dev', - 'rel': 'noopener noreferrer', - }, - classes: 'highlight-link', - [text('ai.fluttercommunity.dev')], - ), - ], - ), - div( - classes: 'volunteer-note', + 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' + }, [ - span( - classes: 'emoji', - [text('🌱')], - ), - p([ - text( - 'This space is volunteer-built with ❤️ — we aim to make agentic Flutter development more accessible for everyone.', - ), - ]), - ], - ), - ], - ), + text('💬 Join the Forum') + ]), + ]), + ], + ), + // Simple, centered volunteer note (no longer in a card) + p( + styles: Styles.raw({ + 'text-align': 'center', + 'color': 'var(--secondary-text)', + 'margin-top': 'var(--spacing-lg)', + 'font-size': '0.9em' /* Optional: slightly smaller text for a footnote feel */ + }), + [ + 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/web/css/styles.css b/web/css/styles.css index 8f809b8..af41498 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -168,13 +168,16 @@ 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 { +.hero .button { /* This rule seems to style text within a .button class, not the container */ font-size: 18px; } From 4c9bdba2b2f9bec6c966ec59a74a4c603f68c671 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 20:23:13 +0200 Subject: [PATCH 19/31] feat: enhance footer component with 'Coming soon' links and tooltip styling * Update footer links for 'Privacy Policy' and 'Code of Conduct' to indicate they are coming soon, improving user experience. * Implement tooltip functionality for 'Coming soon' links to provide additional context, enhancing accessibility. * Add CSS styles for 'Coming soon' links, ensuring they visually align with inactive link standards and include hover effects for tooltips. --- lib/src/components/footer.dart | 15 +++++++++++--- web/css/styles.css | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/lib/src/components/footer.dart b/lib/src/components/footer.dart index da9c850..a804116 100644 --- a/lib/src/components/footer.dart +++ b/lib/src/components/footer.dart @@ -28,8 +28,8 @@ class Footer extends StatelessComponent { FooterColumn( title: 'Legal', links: [ - (path: '#', label: 'Privacy Policy'), - (path: '#', label: 'Code of Conduct'), + (path: '#coming-soon', label: 'Privacy Policy'), + (path: '#coming-soon', label: 'Code of Conduct'), ], ), ]), @@ -88,7 +88,16 @@ class FooterColumn extends StatelessComponent { ul([ for (var link in links) // li([ - if (link.path.startsWith('http')) + 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, diff --git a/web/css/styles.css b/web/css/styles.css index af41498..0e694aa 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -308,6 +308,43 @@ footer a:hover, a.selected { color: var(--accent-color); } +/* 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; + } +} + /* Navbar styles */ .navbar { position: fixed; From 68905295ee378ee11b5e44d5ea6fb7efa7f5c174 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 22:41:33 +0200 Subject: [PATCH 20/31] feat: add new external links and enhance resource descriptions in StartersPage * Introduce a new YouTube link for Jhin Lee's Humpday Q&A and a link to the Gemini API function calling documentation in the ExternalLink enum, improving resource accessibility. * Update the description for the Gemini API link to provide clearer context on its purpose and usage. * Revise the description for the Agentic Apps with Flutter resource to offer a more detailed overview of the content, enhancing user engagement. --- lib/src/app/external_links.dart | 3 +++ lib/src/pages/starters_page.dart | 22 ++++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/lib/src/app/external_links.dart b/lib/src/app/external_links.dart index fda3c5d..ba3623d 100644 --- a/lib/src/app/external_links.dart +++ b/lib/src/app/external_links.dart @@ -3,6 +3,9 @@ enum ExternalLink { 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/pages/starters_page.dart b/lib/src/pages/starters_page.dart index 17358ed..82431d0 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -333,15 +333,25 @@ class StartersPage extends StatelessComponent { title: 'Agentic Apps with Flutter (Observable Flutter #59)', link: 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', - description: 'Deep dive into creating agentic Flutter applications.', - variant: CardVariant.compact, + description: + 'Craig Labenz and Andrew Brogdon discuss and explore AI-powered, agentic apps in Flutter in this Observable Flutter episode.', + variant: CardVariant.greenGlow, ), ResourceLink( emoji: '💬', - title: 'Gemini Function Calling Walkthrough (Google I/O)', - link: 'https://ai.google.dev/gemini-api/docs/function-calling', - description: 'Learn how to implement function calling with Gemini.', - variant: CardVariant.compact, + title: 'Gemini API: Function Calling Docs', + link: ExternalLink.geminiFunctionCallingDocs.url, + 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.', + variant: CardVariant.blueGlow, + ), + ResourceLink( + emoji: '🎥', + 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).', + link: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, + variant: CardVariant.orangeGlow, ), ], ), From 5ea20333fc08328ac78b387dc890d2c1e6c29e3a Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 22:41:41 +0200 Subject: [PATCH 21/31] feat: add Privacy Policy and Code of Conduct pages with routing * Introduce new PrivacyPolicyPage and CodeOfConductPage components, enhancing the website's legal information accessibility. * Update the website and footer routing to include links to the new pages, improving user navigation and compliance visibility. * Ensure proper integration of new routes within the existing application structure for seamless user experience. --- lib/src/app/website.dart | 4 + lib/src/components/footer.dart | 6 +- lib/src/pages/code_of_conduct_page.dart | 52 +++++++++++ lib/src/pages/privacy_policy_page.dart | 110 ++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 2 deletions(-) create mode 100644 lib/src/pages/code_of_conduct_page.dart create mode 100644 lib/src/pages/privacy_policy_page.dart diff --git a/lib/src/app/website.dart b/lib/src/app/website.dart index c39389a..c238444 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'; @@ -26,6 +28,8 @@ class _SiteState extends State { ...HomePage.route(), ...StartersPage.route(), ...BuildersPage.route(), + ...PrivacyPolicyPage.route(), + ...CodeOfConductPage.route(), ], ); } diff --git a/lib/src/components/footer.dart b/lib/src/components/footer.dart index a804116..1475e64 100644 --- a/lib/src/components/footer.dart +++ b/lib/src/components/footer.dart @@ -3,6 +3,8 @@ 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'; class Footer extends StatelessComponent { @override @@ -28,8 +30,8 @@ class Footer extends StatelessComponent { FooterColumn( title: 'Legal', links: [ - (path: '#coming-soon', label: 'Privacy Policy'), - (path: '#coming-soon', label: 'Code of Conduct'), + (path: PrivacyPolicyPage.path, label: 'Privacy Policy'), + (path: CodeOfConductPage.path, label: 'Code of Conduct'), ], ), ]), 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..ecdb98e --- /dev/null +++ b/lib/src/pages/code_of_conduct_page.dart @@ -0,0 +1,52 @@ +import 'package:jaspr/ui.dart'; +import 'package:jaspr_router/jaspr_router.dart'; +import 'package:fc_ai_circle/src/layouts/page_layout.dart'; +import 'package:fc_ai_circle/src/components/content_section.dart'; + +class CodeOfConductPage extends StatelessComponent { + const CodeOfConductPage({super.key}); + + static const path = '/code-of-conduct'; + + static Iterable route() sync* { + yield Route( + path: path, + title: 'Code of Conduct - Flutter Community AI Circle', + builder: (BuildContext context, RouteState state) => CodeOfConductPage(), + ); + } + + @override + Iterable build(BuildContext context) sync* { + yield PageLayout( + children: [ + ContentSection( + title: 'Code of Conduct', + children: [ + p([ + text( + 'Flutter Community AI Circle members are dedicated to providing a harassment-free and inclusive experience ' + 'for everyone regardless of gender identity and expression, sexual orientation, ' + 'disabilities, neurodiversity, physical appearance, body size, ethnicity, ' + 'nationality, race, age, religion, or other protected category.', + ) + ]), + p( + [ + text( + 'Flutter Community AI Circle follows the ', + ), + a( + href: 'https://flutter.dev/culture', + target: Target.blank, + [text('Google Flutter Culture of Inclusivity policies')], + ), + text('.'), + ], + ), + ], + ), + ], + ); + } +} diff --git a/lib/src/pages/privacy_policy_page.dart b/lib/src/pages/privacy_policy_page.dart new file mode 100644 index 0000000..363c821 --- /dev/null +++ b/lib/src/pages/privacy_policy_page.dart @@ -0,0 +1,110 @@ +import 'package:jaspr/ui.dart'; +import 'package:jaspr_router/jaspr_router.dart'; +import 'package:fc_ai_circle/src/layouts/page_layout.dart'; +import 'package:fc_ai_circle/src/components/content_section.dart'; + +class PrivacyPolicyPage extends StatelessComponent { + const PrivacyPolicyPage({super.key}); + + static const path = '/privacy-policy'; + + static Iterable route() sync* { + yield Route( + path: path, + title: 'Privacy Policy - Flutter Community AI Circle', + builder: (BuildContext context, RouteState state) => PrivacyPolicyPage(), + ); + } + + @override + Iterable build(BuildContext context) sync* { + yield PageLayout( + children: [ + ContentSection( + title: 'Privacy Policy', + children: [ + p([text('Effective Date: 29-Aug-2023')]), + h2([text('Who we are')]), + div( + styles: Styles( + padding: Spacing.all(Unit.em(1)), + color: Colors.white, + backgroundColor: Color.value(0x555555), + ), + [text('Our website address is: https://fluttercommunityaicircle.org')], + ), + Spacer(height: Unit.em(1)), + h2([text('App Data')]), + p([ + text( + 'Our published apps do NOT currently collect any data. ' + 'If in the future we create apps which collect data, ' + 'we will update this Privacy Policy.', + ) + ]), + p([ + text( + 'Our published apps do NOT transmit to or store ' + 'any data to our website or server(s).', + ) + ]), + p([ + text( + 'Any apps in testing may collect usage and/or crash ' + 'data, if you choose to share this data it will be ' + 'transmitted to us.', + ) + ]), + Spacer(height: Unit.em(1)), + h2([text('Website Data')]), + p([ + text( + 'Data transmitted via our Contact Us page links ' + 'are managed by the respective sites.', + ), + div( + styles: Styles( + flexDirection: FlexDirection.column, + lineHeight: Unit.em(2.5), + margin: Spacing.only(left: Unit.em(2.0)), + ), + [ + a( + href: 'https://twitter.com/en/privacy', + target: Target.blank, + [text('Twitter/X')], + ), + ], + ), + ]), + p([ + text('We use '), + a( + href: 'https://analytics.google.com/', + target: Target.blank, + [text('basic Google Analytics')], + ), + text(' on our website to understand how our site is used. You can see '), + a( + href: 'https://policies.google.com/privacy', + target: Target.blank, + [text('Google\'s privacy policy')], + ), + text( + ' for more details. The lowest level of detail we see is, ' + 'if the data is available:', + ), + ]), + ul([ + li([text('Acquisition: How you reached our site (ex. direct or search)')]), + li([text('Demographics: Language, Country, and City (NOT your specific location)')]), + li([text('Mobile: Device brand, model, marketing name, and screen resolution')]), + li([text('Technology: Browser & operating system and Internet service provider')]), + ]), + Spacer(height: Unit.em(3.0)), + ], + ), + ], + ); + } +} From 873a25316a709e6084052dc413a13ab8c7df5a3d Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 23:28:26 +0200 Subject: [PATCH 22/31] feat: enhance BuildersPage with new survey section and resource links * Introduce a new _SurveySection component to encourage community feedback and participation, improving user engagement. * Update BuildersPage to include the new survey section, enhancing the visibility of community contributions. * Revise resource links and descriptions for clarity and accessibility, ensuring users have easy access to relevant resources. * Add CSS styles for clickable resource cards, improving the overall user experience and interaction with resource links. --- lib/src/app/website.dart | 2 + lib/src/components/resource_link.dart | 17 +- lib/src/pages/builders_page.dart | 344 +++++++++++++++++--------- web/css/clickable-cards.css | 114 +++++++++ web/js/clickable-cards.js | 51 ++++ 5 files changed, 395 insertions(+), 133 deletions(-) create mode 100644 web/css/clickable-cards.css create mode 100644 web/js/clickable-cards.js diff --git a/lib/src/app/website.dart b/lib/src/app/website.dart index c238444..9f5caf7 100644 --- a/lib/src/app/website.dart +++ b/lib/src/app/website.dart @@ -20,6 +20,8 @@ class _SiteState extends State { children: [ link(href: '/css/fontawesome.css', rel: 'stylesheet'), link(href: '/css/brands.css', rel: 'stylesheet'), + link(href: '/css/clickable-cards.css', rel: 'stylesheet'), + // script(src: '/js/clickable-cards.js', attributes: {'defer': 'true'}, []), // Commented out // Style(styles: resetStyles) ], ); diff --git a/lib/src/components/resource_link.dart b/lib/src/components/resource_link.dart index a749f8d..925bc1b 100644 --- a/lib/src/components/resource_link.dart +++ b/lib/src/components/resource_link.dart @@ -63,10 +63,14 @@ class ResourceLink extends StatelessComponent { yield li( attributes: {'role': 'listitem'}, [ - div( + a( + href: link, + target: Target.blank, classes: variantClasses.join(' '), attributes: { - // Add a data attribute for card height calculation in masonry grid + 'role': 'article', + 'aria-label': title + (description != null ? " - $description" : ""), + 'rel': 'noopener noreferrer', 'data-card-height': '${description != null ? (description!.length ~/ 2) + 120 : 80}', }, [ @@ -74,17 +78,10 @@ class ResourceLink extends StatelessComponent { div( classes: 'resource-content', [ - a( - href: link, - target: Target.blank, - attributes: { - 'aria-label': '$title - ${description ?? ''}', - 'rel': 'noopener noreferrer', - }, + span( classes: 'resource-title', [ text(title), - // The arrow is now added via CSS :after pseudo-element ], ), if (description != null && variant != CardVariant.compact) diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index bae622a..ce9c027 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -2,6 +2,7 @@ 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/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'; @@ -14,7 +15,7 @@ class BuildersPage extends StatelessComponent { yield Route( path: path, title: 'Builders - Flutter AI Builders Circle', - builder: (BuildContext context, RouteState state) => BuildersPage(), + builder: (BuildContext context, RouteState state) => const BuildersPage(), ); } @@ -27,8 +28,8 @@ class BuildersPage extends StatelessComponent { _buildMissionSection(), _buildWhatWeDoSection(), _buildToolkitSection(), - _buildJoinSection(), - _buildContributorsSection(), + const _SurveySection(), + _buildContributorsSection(context), ], ); } @@ -43,8 +44,7 @@ class BuildersPage extends StatelessComponent { children: [ p([ text( - 'Join us in creating agentic Flutter applications with advanced LLM integration, multimodal processing, and cutting-edge tools.', - ), + 'Join us in creating agentic Flutter applications with advanced LLM integration, multimodal processing, and cutting-edge tools.') ]), ], ), @@ -63,10 +63,11 @@ class BuildersPage extends StatelessComponent { children: [ p([ text( - '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).', - ) + '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.') ]), ], ), @@ -80,18 +81,41 @@ class BuildersPage extends StatelessComponent { id: 'mission', emoji: '🎯', children: [ - ContentCard( - children: [ - ul([ - li([ - text( - 'Create accessible resources for Flutter developers to incorporate AI into their apps', - ) - ]), - 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')]), - ]), + ResourceCategory( + title: 'Community Goals', + accent: CategoryAccent.purple, + useGrid: true, + resources: [ + ResourceLink( + emoji: '📚', + title: 'Accessible Resources', + description: + 'Create clear documentation, tutorials, and examples for Flutter developers to incorporate AI into their apps', + link: ExternalLink.forumCategory.url, + variant: CardVariant.highlighted, + ), + ResourceLink( + emoji: '🧪', + title: 'Best Practices', + description: + 'Explore and share responsible AI implementation patterns with the community', + link: ExternalLink.youTubeAgenticQA.url, + variant: CardVariant.blueGlow, + ), + ResourceLink( + emoji: '🤝', + title: 'Supportive Environment', + description: + 'Build a welcoming community for experimentation, learning, and collaboration', + link: ExternalLink.forumChat.url, + ), + ResourceLink( + emoji: '📦', + title: 'Open Source', + description: 'Develop starter kits, reference implementations, and shared resources', + link: ExternalLink.socialGitHub.url, + variant: CardVariant.orangeGlow, + ), ], ), ], @@ -132,6 +156,43 @@ class BuildersPage extends StatelessComponent { ), ], ), + div(styles: Styles(height: Unit.em(2)), []), + 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( + emoji: '🎬', + title: 'AI Circle Playlist', + description: 'Collection of our community sessions and tutorials', + link: ExternalLink.youTubePlaylist.url, + variant: CardVariant.highlighted, + ), + ResourceLink( + emoji: '🎮', + title: 'Vibe Coding Sessions', + description: 'Relaxed coding streams building AI-powered apps', + link: ExternalLink.youTubeVibeCoding.url, + variant: CardVariant.greenGlow, + ), + ResourceLink( + emoji: '🧠', + title: 'Agentic Q&A Session', + description: 'Deep dive into agentic app patterns', + link: ExternalLink.youTubeAgenticQA.url, + variant: CardVariant.orangeGlow, + ), + ], + ), + ], + ), ], ); } @@ -144,62 +205,51 @@ class BuildersPage extends StatelessComponent { description: 'Resources and tools to help you build agentic Flutter applications.', children: [ ResourceCategory( - title: 'Essential Tools', + title: 'Essential Resources', accent: CategoryAccent.purple, useGrid: true, resources: [ ResourceLink( emoji: '🔄', - title: 'Function Calling Patterns', - description: 'Reference implementations of function calling with Gemini and Claude.', - link: '#', - // TODO: Add link to function calling patterns documentation once available + title: 'Function Calling with Gemini', + description: + 'Official documentation for implementing function calling with Gemini AI', + link: ExternalLink.geminiFunctionCallingDocs.url, variant: CardVariant.highlighted, ), ResourceLink( - emoji: '🔍', - title: 'RAG Implementation Guide', - description: 'Step-by-step guide to implementing retrieval-augmented generation.', - link: '#', - // TODO: Add link to RAG implementation guide once published + emoji: '🎥', + title: 'MCP for Flutter Developers', + description: 'Understanding the Model Context Protocol for Flutter applications', + link: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, variant: CardVariant.blueGlow, ), ], ), ResourceCategory( - title: 'Advanced Workflows', + title: 'Community Channels', accent: CategoryAccent.green, - useHorizontalScroll: true, + useGrid: true, resources: [ ResourceLink( - emoji: '🧠', - title: 'Agent Orchestration', - description: 'Patterns for coordinating multiple AI agents in a Flutter app.', - link: '#', - // TODO: Add link to agent orchestration documentation once available + emoji: '📺', + title: 'YouTube Channel', + description: 'Tutorials, livestreams, and coding sessions from our community', + link: ExternalLink.youTubeFlutterCommunity.url, variant: CardVariant.highlighted, ), ResourceLink( - emoji: '📱', - title: 'On-Device LLM Integration', - description: 'Guide to running smaller language models locally in Flutter.', - link: '#', - // TODO: Add link to on-device LLM integration guide once published + emoji: '💬', + title: 'Flutter Forum', + description: 'Discussions, Q&A, and knowledge sharing about Flutter and AI', + link: ExternalLink.forumCategory.url, variant: CardVariant.greenGlow, ), ResourceLink( - emoji: '🔊', - title: 'Voice Input Processing', - description: 'Integrating speech recognition with LLM processing pipelines.', - link: '#', - // TODO: Add link to voice processing tutorial once available - ), - ResourceLink( - emoji: '📷', - title: 'Vision + LLM', - description: 'Combining image processing with language model understanding.', - link: '#', - // TODO: Add link to vision+LLM integration guide once published + emoji: '📝', + title: 'Community Survey', + description: 'Help shape the future of our community by sharing your feedback', + link: ExternalLink.surveyCommunity.url, variant: CardVariant.orangeGlow, ), ], @@ -208,62 +258,24 @@ class BuildersPage extends StatelessComponent { ); } - Component _buildJoinSection() { - return ContentSection( - title: 'Join Us', - id: 'join', - emoji: '✨', - elevated: true, - children: [ - ContentCard( - elevated: true, - animateCta: true, - children: [ - p([ - text( - 'We\'re currently preparing our community platforms. Join us to collaborate, ' - 'learn, and build the future of AI-powered Flutter apps.', - ) - ]), - div( - classes: 'coming-next-content', - [ - div( - classes: 'volunteer-note', - [ - span( - classes: 'emoji', - [text('🔔')], - ), - p([ - text( - 'Sign up for our newsletter to get notified when our community platforms are ready.', - ), - ]), - ], - ), - div( - classes: 'buttons-container', - [ - a( - href: '#', - // TODO: Add link to community signup form once available - classes: 'cta_button', - [ - span([text('Join the Community')]), - ], - ), - ], - ), - ], - ), - ], - ), - ], + // Helper method to create a resource card (no special click logic here anymore) + ResourceLink _buildClickableCard({ + required String emoji, + required String title, + required String link, + String? description, + CardVariant variant = CardVariant.standard, + }) { + return ResourceLink( + emoji: emoji, + title: title, + description: description, + link: link, + variant: variant, ); } - Component _buildContributorsSection() { + Component _buildContributorsSection(BuildContext context) { return ContentSection( title: 'Want to Contribute?', id: 'contributors', @@ -278,50 +290,79 @@ class BuildersPage extends StatelessComponent { ResourceLink( emoji: '📝', title: 'Write Documentation', - description: 'Help create clear guides, examples, and tutorials.', - link: '#', - // TODO: Add link to documentation contribution guidelines once available + description: 'Help create clear guides, examples, and tutorials for the community', + link: ExternalLink.socialGitHub.url, variant: CardVariant.blueGlow, ), ResourceLink( emoji: '🧪', title: 'Create Example Apps', - description: 'Build sample projects demonstrating AI integration patterns.', - link: '#', - // TODO: Add link to example app contribution guidelines once available + description: 'Build sample projects demonstrating AI integration patterns', + link: ExternalLink.socialGitHub.url, variant: CardVariant.highlighted, ), ResourceLink( emoji: '🔍', title: 'Review Code', - description: 'Help ensure quality and consistency in our code samples.', - link: '#', - // TODO: Add link to code review guidelines once published + description: 'Help ensure quality and consistency in our code samples', + link: ExternalLink.socialGitHub.url, ), ResourceLink( emoji: '🌐', title: 'Spread the Word', - description: 'Share our resources with your network and community.', - link: '#', - // TODO: Add link to community outreach resources once available + description: 'Share our resources with your network and community', + link: ExternalLink.surveyCommunity.url, ), ], ), ContentCard( + elevated: true, + animateCta: true, children: [ - p( - classes: 'section-description', - [text('PRs welcome - watch this space for contribution guidelines.')], + div( + classes: 'volunteer-note', + [ + span(classes: 'emoji', [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')]) + ], + ), + ], + ), ], ), ], @@ -357,3 +398,60 @@ class _FeatureCard extends StatelessComponent { ); } } + +// New private component for the Survey Section +class _SurveySection extends StatelessComponent { + const _SurveySection({super.key}); + + @override + Iterable build(BuildContext context) sync* { + yield div( + classes: 'container', + [ + div( + classes: 'survey-callout', + [ + ContentCard( + elevated: true, + animateCta: true, + children: [ + div( + classes: 'survey-header', + [ + span(classes: 'emoji large-emoji', [text('🔔')]), + h2(classes: 'survey-title', [ + text( + 'Take our community survey to help shape the future of the Flutter Community AI Circle') + ]), + ], + ), + p( + styles: + Styles(color: Colors.white), // Retaining explicit white color for this text + [ + text( + '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.') + ], + ), + div( + classes: 'buttons-container centered-buttons', + [ + a( + href: ExternalLink.surveyCommunity.url, + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + classes: 'cta_button pulse-button', + [ + span([text('Take the Survey')]) + ], + ), + ], + ), + ], + ), + ], + ), + ], + ); + } +} diff --git a/web/css/clickable-cards.css b/web/css/clickable-cards.css new file mode 100644 index 0000000..b54237c --- /dev/null +++ b/web/css/clickable-cards.css @@ -0,0 +1,114 @@ +/* Make resource cards fully clickable */ +/* The .resource-link class is now on an tag */ +a.resource-link { + display: flex; /* Or block, depending on desired inner layout. Flex is often good for card-like structures. */ + flex-direction: row; /* Align emoji and content side-by-side */ + align-items: center; /* Vertically align items if flex-direction is row */ + padding: 1rem; /* Example padding, adjust as needed */ + border: 1px solid var(--border-color, #ccc); /* Example border */ + border-radius: 8px; /* Example border-radius */ + text-decoration: none; /* Remove default underline from the main link */ + color: inherit; /* Inherit text color from parent, or set explicitly */ + position: relative; + transition: transform 0.2s ease, box-shadow 0.2s ease; + /* cursor: pointer; */ /* Browsers usually do this for tags by default */ +} + +a.resource-link:hover { + transform: translateY(-3px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); + /* Add other hover styles as needed, e.g., border-color */ +} + +/* Style for the title, which is now a span */ +.resource-link .resource-title { + /* position: relative; */ /* z-index from before is not needed */ + font-weight: bold; /* Example: make title bold */ + margin-bottom: 0.25em; /* Example spacing */ + /* Ensure it doesn't pick up link colors if not desired */ +} + +/* Adjust emoji and content div styling if needed due to flex layout on a.resource-link */ +.resource-link .resource-emoji { + margin-right: 0.75em; /* Space between emoji and content */ + font-size: 1.5em; /* Example size */ +} + +.resource-link .resource-content { + display: flex; + flex-direction: column; /* Stack title and description vertically */ + flex: 1; /* Allow content to take remaining space */ +} + +.resource-link .resource-description { + font-size: 0.9em; + color: var(--text-secondary-color, #555); /* Example color */ + 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 { + margin: 3rem 0; + border-radius: 12px; + overflow: hidden; + background-color: var(--primary-color); /* Add a background color */ +} + +.survey-header { + display: flex; + align-items: center; + margin-bottom: 1rem; + gap: 1rem; +} + +.large-emoji { + font-size: 2.5rem; + padding: 0.5rem; + display: inline-block; +} + +.survey-title { + margin: 0; + font-size: 1.8rem; + line-height: 1.3; + color: white; /* Changed from var(--primary-color) to white */ +} + +.centered-buttons { + display: flex; + justify-content: center; + margin-top: 1.5rem; +} + +.pulse-button { + 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); + } +} 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.'); +}); From f9e04f9a9bef4ffd96754e0fd0514cc6e7c37ca5 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 11 May 2025 23:31:56 +0200 Subject: [PATCH 23/31] feat: update survey section in BuildersPage for improved user engagement * Refactor _SurveySection component to enhance the survey callout design and improve accessibility. * Update CSS styles for the survey callout, including new container classes and improved layout for better visual presentation. * Adjust survey title and description for clarity, encouraging community participation and feedback. * Ensure consistent styling and spacing for buttons within the survey section, enhancing user interaction. --- lib/src/pages/builders_page.dart | 62 +++++++++++++++----------------- web/css/clickable-cards.css | 37 ++++++++++++++----- 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index ce9c027..b5348c1 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -406,45 +406,39 @@ class _SurveySection extends StatelessComponent { @override Iterable build(BuildContext context) sync* { yield div( - classes: 'container', + classes: 'survey-callout-container', [ div( classes: 'survey-callout', [ - ContentCard( - elevated: true, - animateCta: true, - children: [ - div( - classes: 'survey-header', - [ - span(classes: 'emoji large-emoji', [text('🔔')]), - h2(classes: 'survey-title', [ - text( - 'Take our community survey to help shape the future of the Flutter Community AI Circle') - ]), - ], - ), - p( - styles: - Styles(color: Colors.white), // Retaining explicit white color for this text - [ - text( - '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.') - ], - ), - div( - classes: 'buttons-container centered-buttons', + div( + classes: 'survey-header', + [ + span( + classes: 'emoji large-emoji', + attributes: {'aria-hidden': 'true'}, + [text('🔔')]), + h2(classes: 'survey-title', [text('Shape Our Community!')]), + ], + ), + p( + classes: 'survey-description', + styles: Styles(color: Colors.white), + [ + text( + '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.') + ], + ), + div( + classes: 'buttons-container centered-buttons', + [ + a( + href: ExternalLink.surveyCommunity.url, + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + classes: 'cta_button pulse-button', [ - a( - href: ExternalLink.surveyCommunity.url, - target: Target.blank, - attributes: {'rel': 'noopener noreferrer'}, - classes: 'cta_button pulse-button', - [ - span([text('Take the Survey')]) - ], - ), + span([text('Take the Survey')]) ], ), ], diff --git a/web/css/clickable-cards.css b/web/css/clickable-cards.css index b54237c..1bb5c5c 100644 --- a/web/css/clickable-cards.css +++ b/web/css/clickable-cards.css @@ -64,31 +64,50 @@ a.resource-link:hover { } /* 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 { - margin: 3rem 0; - border-radius: 12px; - overflow: hidden; - background-color: var(--primary-color); /* Add a background color */ + 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: 1rem; + margin-bottom: 1.25rem; /* Increased spacing */ gap: 1rem; } .large-emoji { - font-size: 2.5rem; - padding: 0.5rem; + 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.8rem; + font-size: 1.6rem; /* Adjusted for new shorter title */ + font-weight: 600; /* Bolder title */ line-height: 1.3; - color: white; /* Changed from var(--primary-color) to white */ + 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 { From f08fd895bed07b27ccf87fd41a53d869d9a1a1ff Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Mon, 12 May 2025 00:09:54 +0200 Subject: [PATCH 24/31] feat: enhance navigation and footer functionality for improved user experience * Integrate JavaScript for automatic scroll reset on navigation, ensuring users start at the top of the page after navigating to internal links. * Update Footer component to include scroll reset functionality on link clicks, enhancing navigation consistency. * Refactor TakeSurvey component to allow customizable button text and additional CSS classes, improving flexibility and user engagement. * Revise resource links in BuildersPage to utilize new SurveyCallout component, streamlining the layout and enhancing visual appeal. * Update CSS styles for clickable cards and section spacers, ensuring a cohesive design across the application. --- lib/src/components/feature_card.dart | 42 +++++ lib/src/components/footer.dart | 9 +- lib/src/components/resource_link.dart | 4 +- lib/src/components/survey_callout.dart | 76 ++++++++ lib/src/components/take_survey.dart | 19 +- lib/src/pages/builders_page.dart | 115 ++---------- lib/src/pages/code_of_conduct_page.dart | 125 ++++++++++--- lib/src/pages/home_page.dart | 5 +- lib/src/pages/privacy_policy_page.dart | 166 +++++++++-------- lib/src/pages/starters_page.dart | 60 +++--- web/css/clickable-cards.css | 45 ++--- web/css/styles.css | 237 +++++++++++------------- web/main.dart | 67 +++++++ 13 files changed, 570 insertions(+), 400 deletions(-) create mode 100644 lib/src/components/feature_card.dart create mode 100644 lib/src/components/survey_callout.dart 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 1475e64..dd9b10b 100644 --- a/lib/src/components/footer.dart +++ b/lib/src/components/footer.dart @@ -5,6 +5,7 @@ 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 @@ -112,7 +113,13 @@ class FooterColumn extends StatelessComponent { else a( href: link.path, - onClick: () => Router.of(context).push(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/resource_link.dart b/lib/src/components/resource_link.dart index 925bc1b..21b802b 100644 --- a/lib/src/components/resource_link.dart +++ b/lib/src/components/resource_link.dart @@ -1,5 +1,3 @@ -import 'dart:async'; -import 'dart:html' as html; import 'package:jaspr/browser.dart'; enum CardVariant { @@ -69,7 +67,7 @@ class ResourceLink extends StatelessComponent { classes: variantClasses.join(' '), attributes: { 'role': 'article', - 'aria-label': title + (description != null ? " - $description" : ""), + 'aria-label': title + (description != null ? ' - $description' : ''), 'rel': 'noopener noreferrer', 'data-card-height': '${description != null ? (description!.length ~/ 2) + 120 : 80}', }, 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 b5348c1..ecd8ed9 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -1,16 +1,20 @@ 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/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 const path = '/builders'; + /// Defines the routing for this page static Iterable route() sync* { yield Route( path: path, @@ -28,7 +32,7 @@ class BuildersPage extends StatelessComponent { _buildMissionSection(), _buildWhatWeDoSection(), _buildToolkitSection(), - const _SurveySection(), + const SurveyCallout(), // Using our new component _buildContributorsSection(context), ], ); @@ -132,31 +136,31 @@ class BuildersPage extends StatelessComponent { classes: 'features-grid', attributes: {'role': 'list'}, [ - _FeatureCard( + FeatureCard( title: 'Share Knowledge', description: 'Technical articles, code examples, and implementation guides', isHighlighted: true, emoji: '📚', ), - _FeatureCard( + FeatureCard( title: 'Build Tools', description: 'Open-source packages and utilities to simplify AI integration', emoji: '🔧', ), - _FeatureCard( + FeatureCard( title: 'Provide Templates', description: 'Ready-to-use starter projects for common AI tasks', isHighlighted: true, emoji: '📋', ), - _FeatureCard( + FeatureCard( title: 'Foster Collaboration', description: 'Connect developers working on similar challenges', emoji: '🤝', ), ], ), - div(styles: Styles(height: Unit.em(2)), []), + div(classes: 'section-spacer', []), ContentSection( title: 'YouTube Content', emoji: '💡', @@ -258,23 +262,6 @@ class BuildersPage extends StatelessComponent { ); } - // Helper method to create a resource card (no special click logic here anymore) - ResourceLink _buildClickableCard({ - required String emoji, - required String title, - required String link, - String? description, - CardVariant variant = CardVariant.standard, - }) { - return ResourceLink( - emoji: emoji, - title: title, - description: description, - link: link, - variant: variant, - ); - } - Component _buildContributorsSection(BuildContext context) { return ContentSection( title: 'Want to Contribute?', @@ -322,7 +309,7 @@ class BuildersPage extends StatelessComponent { div( classes: 'volunteer-note', [ - span(classes: 'emoji', [text('✋')]), + span(classes: 'emoji', attributes: {'aria-hidden': 'true'}, [text('✋')]), h3([text('Ready to contribute?')]), p([ text( @@ -369,83 +356,3 @@ class BuildersPage extends StatelessComponent { ); } } - -class _FeatureCard extends StatelessComponent { - const _FeatureCard({ - required this.title, - required this.description, - this.isHighlighted = false, - this.emoji, - }); - - final String title; - final String description; - final bool isHighlighted; - 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', [text('$emoji ')]), - text(title), - ]), - p([text(description)]), - ], - ); - } -} - -// New private component for the Survey Section -class _SurveySection extends StatelessComponent { - const _SurveySection({super.key}); - - @override - Iterable build(BuildContext context) sync* { - yield div( - classes: 'survey-callout-container', - [ - div( - classes: 'survey-callout', - [ - div( - classes: 'survey-header', - [ - span( - classes: 'emoji large-emoji', - attributes: {'aria-hidden': 'true'}, - [text('🔔')]), - h2(classes: 'survey-title', [text('Shape Our Community!')]), - ], - ), - p( - classes: 'survey-description', - styles: Styles(color: Colors.white), - [ - text( - '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.') - ], - ), - div( - classes: 'buttons-container centered-buttons', - [ - a( - href: ExternalLink.surveyCommunity.url, - target: Target.blank, - attributes: {'rel': 'noopener noreferrer'}, - classes: 'cta_button pulse-button', - [ - span([text('Take the Survey')]) - ], - ), - ], - ), - ], - ), - ], - ); - } -} diff --git a/lib/src/pages/code_of_conduct_page.dart b/lib/src/pages/code_of_conduct_page.dart index ecdb98e..68a1a08 100644 --- a/lib/src/pages/code_of_conduct_page.dart +++ b/lib/src/pages/code_of_conduct_page.dart @@ -1,18 +1,20 @@ -import 'package:jaspr/ui.dart'; -import 'package:jaspr_router/jaspr_router.dart'; -import 'package:fc_ai_circle/src/layouts/page_layout.dart'; 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 Community AI Circle', - builder: (BuildContext context, RouteState state) => CodeOfConductPage(), + title: 'Code of Conduct - Flutter AI Builders Circle', + builder: (BuildContext context, RouteState state) => const CodeOfConductPage(), ); } @@ -22,26 +24,101 @@ class CodeOfConductPage extends StatelessComponent { children: [ ContentSection( title: 'Code of Conduct', + id: 'code-of-conduct', + emoji: '🤝', children: [ - p([ - text( - 'Flutter Community AI Circle members are dedicated to providing a harassment-free and inclusive experience ' - 'for everyone regardless of gender identity and expression, sexual orientation, ' - 'disabilities, neurodiversity, physical appearance, body size, ethnicity, ' - 'nationality, race, age, religion, or other protected category.', - ) - ]), - p( - [ - text( - 'Flutter Community AI Circle follows the ', - ), - a( - href: 'https://flutter.dev/culture', - target: Target.blank, - [text('Google Flutter Culture of Inclusivity policies')], - ), - text('.'), + 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 61f3408..e241169 100644 --- a/lib/src/pages/home_page.dart +++ b/lib/src/pages/home_page.dart @@ -96,8 +96,9 @@ class _FeaturesSection extends StatelessComponent { div(classes: 'features-grid', [ _FeatureCard( title: 'Past Livestream', - description: 'Vibe Coding a Card Game with Norbert & Friends', - url: ExternalLink.youTubeVibeCoding.url, + description: 'FCAIC #1 - Prompt, Code, Think: Welcome to AI in Flutter', + url: + 'https://www.youtube.com/watch?v=Gub1DY8ScwU&list=PL4dBIh1xps-HIYvaEIbLWHZqt_WGBfpx3', ), _FeatureCard( title: 'Agentic Apps Spotlight', diff --git a/lib/src/pages/privacy_policy_page.dart b/lib/src/pages/privacy_policy_page.dart index 363c821..aab4959 100644 --- a/lib/src/pages/privacy_policy_page.dart +++ b/lib/src/pages/privacy_policy_page.dart @@ -1,18 +1,20 @@ -import 'package:jaspr/ui.dart'; -import 'package:jaspr_router/jaspr_router.dart'; -import 'package:fc_ai_circle/src/layouts/page_layout.dart'; 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 Community AI Circle', - builder: (BuildContext context, RouteState state) => PrivacyPolicyPage(), + title: 'Privacy Policy - Flutter AI Builders Circle', + builder: (BuildContext context, RouteState state) => const PrivacyPolicyPage(), ); } @@ -22,86 +24,88 @@ class PrivacyPolicyPage extends StatelessComponent { children: [ ContentSection( title: 'Privacy Policy', + id: 'privacy-policy', + emoji: '🔒', children: [ - p([text('Effective Date: 29-Aug-2023')]), - h2([text('Who we are')]), - div( - styles: Styles( - padding: Spacing.all(Unit.em(1)), - color: Colors.white, - backgroundColor: Color.value(0x555555), - ), - [text('Our website address is: https://fluttercommunityaicircle.org')], - ), - Spacer(height: Unit.em(1)), - h2([text('App Data')]), - p([ - text( - 'Our published apps do NOT currently collect any data. ' - 'If in the future we create apps which collect data, ' - 'we will update this Privacy Policy.', - ) - ]), - p([ - text( - 'Our published apps do NOT transmit to or store ' - 'any data to our website or server(s).', - ) - ]), - p([ - text( - 'Any apps in testing may collect usage and/or crash ' - 'data, if you choose to share this data it will be ' - 'transmitted to us.', - ) - ]), - Spacer(height: Unit.em(1)), - h2([text('Website Data')]), - p([ - text( - 'Data transmitted via our Contact Us page links ' - 'are managed by the respective sites.', - ), - div( - styles: Styles( - flexDirection: FlexDirection.column, - lineHeight: Unit.em(2.5), - margin: Spacing.only(left: Unit.em(2.0)), - ), - [ + 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: 'https://twitter.com/en/privacy', + href: 'mailto:flutter.community@example.com', target: Target.blank, - [text('Twitter/X')], + attributes: {'rel': 'noopener noreferrer'}, + [text('flutter.community@example.com')], ), - ], - ), - ]), - p([ - text('We use '), - a( - href: 'https://analytics.google.com/', - target: Target.blank, - [text('basic Google Analytics')], - ), - text(' on our website to understand how our site is used. You can see '), - a( - href: 'https://policies.google.com/privacy', - target: Target.blank, - [text('Google\'s privacy policy')], - ), - text( - ' for more details. The lowest level of detail we see is, ' - 'if the data is available:', - ), - ]), - ul([ - li([text('Acquisition: How you reached our site (ex. direct or search)')]), - li([text('Demographics: Language, Country, and City (NOT your specific location)')]), - li([text('Mobile: Device brand, model, marketing name, and screen resolution')]), - li([text('Technology: Browser & operating system and Internet service provider')]), - ]), - Spacer(height: Unit.em(3.0)), + text('.'), + ]), + ], + ), ], ), ], diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index 82431d0..2601099 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -60,17 +60,17 @@ class StartersPage extends StatelessComponent { id: 'core-concepts', emoji: '🧩', children: [ - ul( - classes: 'resource-list', - attributes: {'role': 'list'}, - [ + ResourceCategory( + title: 'Key Concepts', + useGrid: true, + resources: [ ResourceLink( emoji: '🧠', title: 'LangChain: Introduction', description: 'Learn the foundations of LangChain and its modular architecture for building LLM-powered agents.', link: 'https://python.langchain.com/docs/introduction/', - variant: CardVariant.highlighted, + variant: CardVariant.standard, ), ResourceLink( emoji: '📘', @@ -85,7 +85,7 @@ class StartersPage extends StatelessComponent { title: 'Introducing the Model Context Protocol (Anthropic)', description: 'Learn how MCP standardizes interactions between LLMs and tools.', link: 'https://www.anthropic.com/news/model-context-protocol', - variant: CardVariant.blueGlow, + variant: CardVariant.standard, ), ResourceLink( emoji: '📚', @@ -101,7 +101,7 @@ class StartersPage extends StatelessComponent { 'Craig Labenz and Andrew Brogdon walk through agentic patterns, Colorist, and Firebase Genkit.', link: 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', - variant: CardVariant.orangeGlow, + variant: CardVariant.standard, ), ], ), @@ -125,7 +125,7 @@ class StartersPage extends StatelessComponent { description: 'Beginner/Intermediate. Learn how to integrate Gemini into a Flutter app using Firebase extensions and function calling.', link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', - variant: CardVariant.highlighted, + variant: CardVariant.standard, ), ResourceLink( emoji: '🤖', @@ -167,7 +167,7 @@ class StartersPage extends StatelessComponent { description: 'Beginner. Transcribe audio to text using OpenAI\'s hosted Whisper model.', link: 'https://platform.openai.com/docs/guides/speech-to-text', - variant: CardVariant.greenGlow, + variant: CardVariant.standard, ), ResourceLink( emoji: '📱', @@ -189,7 +189,7 @@ class StartersPage extends StatelessComponent { description: 'Intermediate. Store and retrieve vector embeddings in Firestore for building RAG pipelines.', link: 'https://extensions.dev/extensions/googlecloud/firestore-vector-search', - variant: CardVariant.orangeGlow, + variant: CardVariant.standard, ), ResourceLink( emoji: '⚡', @@ -197,7 +197,7 @@ class StartersPage extends StatelessComponent { description: 'Advanced. A backend framework for building AI-driven workflows with Gemini, designed for Cloud Functions.', link: 'https://firebase.google.com/docs/genkit', - variant: CardVariant.highlighted, + variant: CardVariant.standard, ), ], ), @@ -251,7 +251,7 @@ class StartersPage extends StatelessComponent { ResourceCategory( title: 'Codelabs, Templates, and Examples', accent: CategoryAccent.green, - useHorizontalScroll: true, + useGrid: true, resources: [ ResourceLink( emoji: '🎨', @@ -259,7 +259,7 @@ class StartersPage extends StatelessComponent { description: 'Intermediate. Gemini-powered Flutter app that updates the UI based on natural language prompts.', link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', - variant: CardVariant.highlighted, + variant: CardVariant.standard, ), ResourceLink( emoji: '📚', @@ -267,7 +267,7 @@ class StartersPage extends StatelessComponent { description: 'Intermediate/Advanced. Google\'s official examples for calling Gemini API across languages including Dart.', link: 'https://github.com/google-gemini/cookbook', - variant: CardVariant.greenGlow, + variant: CardVariant.standard, ), ResourceLink( emoji: '🚀', @@ -295,7 +295,7 @@ class StartersPage extends StatelessComponent { description: 'Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more.', link: 'https://pub.dev/packages/langchain', - variant: CardVariant.highlighted, + variant: CardVariant.standard, ), ResourceLink( emoji: '🤖', @@ -303,7 +303,7 @@ class StartersPage extends StatelessComponent { description: 'Advanced. A collection of Flutter plugins for integrating generative AI capabilities into your applications.', link: 'https://pub.dev/packages/flutter_genai', - variant: CardVariant.blueGlow, + variant: CardVariant.standard, ), ResourceLink( emoji: '📱', @@ -324,10 +324,10 @@ class StartersPage extends StatelessComponent { id: 'deepen-understanding', emoji: '🧠', children: [ - ul( - classes: 'resource-list', - attributes: {'role': 'list'}, - [ + ResourceCategory( + title: 'Additional Resources', + useGrid: true, + resources: [ ResourceLink( emoji: '🎥', title: 'Agentic Apps with Flutter (Observable Flutter #59)', @@ -335,7 +335,7 @@ class StartersPage extends StatelessComponent { 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', description: 'Craig Labenz and Andrew Brogdon discuss and explore AI-powered, agentic apps in Flutter in this Observable Flutter episode.', - variant: CardVariant.greenGlow, + variant: CardVariant.standard, ), ResourceLink( emoji: '💬', @@ -343,7 +343,7 @@ class StartersPage extends StatelessComponent { link: ExternalLink.geminiFunctionCallingDocs.url, 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.', - variant: CardVariant.blueGlow, + variant: CardVariant.standard, ), ResourceLink( emoji: '🎥', @@ -351,7 +351,7 @@ class StartersPage extends StatelessComponent { description: 'GDE Jhin Lee discusses the mcp_dart package and the Model Context Protocol. (MCP segment starts at 29:54).', link: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, - variant: CardVariant.orangeGlow, + variant: CardVariant.standard, ), ], ), @@ -448,12 +448,14 @@ class StartersPage extends StatelessComponent { ), // Simple, centered volunteer note (no longer in a card) p( - styles: Styles.raw({ - 'text-align': 'center', - 'color': 'var(--secondary-text)', - 'margin-top': 'var(--spacing-lg)', - 'font-size': '0.9em' /* Optional: slightly smaller text for a footnote feel */ - }), + 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 '), diff --git a/web/css/clickable-cards.css b/web/css/clickable-cards.css index 1bb5c5c..a810ed9 100644 --- a/web/css/clickable-cards.css +++ b/web/css/clickable-cards.css @@ -1,48 +1,41 @@ /* Make resource cards fully clickable */ /* The .resource-link class is now on an tag */ a.resource-link { - display: flex; /* Or block, depending on desired inner layout. Flex is often good for card-like structures. */ - flex-direction: row; /* Align emoji and content side-by-side */ - align-items: center; /* Vertically align items if flex-direction is row */ - padding: 1rem; /* Example padding, adjust as needed */ - border: 1px solid var(--border-color, #ccc); /* Example border */ - border-radius: 8px; /* Example border-radius */ - text-decoration: none; /* Remove default underline from the main link */ - color: inherit; /* Inherit text color from parent, or set explicitly */ + 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; - /* cursor: pointer; */ /* Browsers usually do this for tags by default */ } a.resource-link:hover { - transform: translateY(-3px); - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1); - /* Add other hover styles as needed, e.g., border-color */ + transform: translateY(-2px); } /* Style for the title, which is now a span */ .resource-link .resource-title { - /* position: relative; */ /* z-index from before is not needed */ - font-weight: bold; /* Example: make title bold */ - margin-bottom: 0.25em; /* Example spacing */ - /* Ensure it doesn't pick up link colors if not desired */ + 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; /* Space between emoji and content */ - font-size: 1.5em; /* Example size */ + margin-right: 0.75em; + font-size: 1.5em; } .resource-link .resource-content { display: flex; - flex-direction: column; /* Stack title and description vertically */ - flex: 1; /* Allow content to take remaining space */ + flex-direction: column; + flex: 1; } .resource-link .resource-description { font-size: 0.9em; - color: var(--text-secondary-color, #555); /* Example color */ + color: var(--text-secondary-color, #555); line-height: 1.4; } @@ -120,6 +113,10 @@ a.resource-link:hover { 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); @@ -131,3 +128,9 @@ a.resource-link:hover { 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 0e694aa..853af88 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -438,14 +438,9 @@ 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 */ @@ -582,6 +577,7 @@ button:focus-visible { position: relative; } +/* Remove the divider .content-section:not(:last-child):after { content: ''; height: 4px; @@ -592,6 +588,7 @@ button:focus-visible { margin: 32px auto 0; display: block; } +*/ .section-content { padding: var(--spacing-md) 0; @@ -633,14 +630,15 @@ button:focus-visible { border-radius: var(--border-radius); margin-bottom: var(--spacing-md); transition: all var(--transition-speed); - border: 1px solid transparent; + 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 20px var(--shadow-color); + box-shadow: 0 0 15px rgba(171, 87, 255, 0.15); + border-color: rgba(171, 87, 255, 0.2); } .resource-emoji { @@ -667,6 +665,7 @@ button:focus-visible { gap: 8px; } +/* Remove the arrow decoration .resource-title:after { content: '→'; font-size: 0.8em; @@ -679,6 +678,7 @@ button:focus-visible { opacity: 1; transform: translateX(0); } +*/ .resource-description { color: var(--secondary-text); @@ -694,22 +694,22 @@ button:focus-visible { /* Category accent colors - using theme variables */ .resource-category.purple-accent { - border-left: 3px solid var(--accent-color); + border-left: 1px solid var(--accent-color); padding-left: var(--spacing-md); } .resource-category.blue-accent { - border-left: 3px solid #4e9fff; + border-left: 1px solid #4e9fff; padding-left: var(--spacing-md); } .resource-category.green-accent { - border-left: 3px solid #57ffab; + border-left: 1px solid #57ffab; padding-left: var(--spacing-md); } .resource-category.orange-accent { - border-left: 3px solid #ff9f4e; + border-left: 1px solid #ff9f4e; padding-left: var(--spacing-md); } @@ -719,28 +719,6 @@ button:focus-visible { margin-bottom: var(--spacing-md); color: var(--accent-color); font-weight: 600; - display: flex; - align-items: center; -} - -.category-title:before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--accent-color); - margin-right: 12px; -} - -.category-title:after { - content: ''; - display: block; - height: 1px; - background: linear-gradient(90deg, var(--accent-color) 0%, transparent 100%); - flex: 1; - margin-left: var(--spacing-md); - opacity: 0.3; } /* Grid layout for resource lists */ @@ -750,35 +728,15 @@ button:focus-visible { gap: var(--spacing-md); } -/* Card variants - consolidating shared 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 transparent; - height: 100%; - box-sizing: border-box; -} - -.resource-link:hover { - transform: translateY(-2px); - box-shadow: 0 0 20px var(--shadow-color); -} - -/* Highlighted variant */ +/* Highlighted variant - more subtle */ .resource-link.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); + 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 20px rgba(171, 87, 255, 0.3); - border-color: rgba(171, 87, 255, 0.4); + box-shadow: 0 0 15px rgba(171, 87, 255, 0.2); + border-color: rgba(171, 87, 255, 0.25); } /* Compact variant */ @@ -796,47 +754,51 @@ button:focus-visible { margin-bottom: 0; } -/* Color variant styles */ +/* Color variant styles - more subtle */ .resource-link.blue-glow { - box-shadow: 0 0 15px rgba(78, 159, 255, 0.15); - border: 1px solid rgba(78, 159, 255, 0.2); - background-color: rgba(78, 159, 255, 0.03); + 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 20px rgba(78, 159, 255, 0.3); - border-color: rgba(78, 159, 255, 0.4); + box-shadow: 0 0 15px rgba(78, 159, 255, 0.2); + border-color: rgba(78, 159, 255, 0.25); } .resource-link.green-glow { - box-shadow: 0 0 15px rgba(87, 255, 171, 0.15); - border: 1px solid rgba(87, 255, 171, 0.2); - background-color: rgba(87, 255, 171, 0.03); + 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 20px rgba(87, 255, 171, 0.3); - border-color: rgba(87, 255, 171, 0.4); + box-shadow: 0 0 15px rgba(87, 255, 171, 0.2); + border-color: rgba(87, 255, 171, 0.25); } .resource-link.orange-glow { - box-shadow: 0 0 15px rgba(255, 159, 78, 0.15); - border: 1px solid rgba(255, 159, 78, 0.2); - background-color: rgba(255, 159, 78, 0.03); + 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 20px rgba(255, 159, 78, 0.3); - border-color: rgba(255, 159, 78, 0.4); + 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; - margin-bottom: var(--spacing-md); } -/* Only enable scrolling on mobile */ +/* 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; @@ -858,32 +820,14 @@ button:focus-visible { flex: 0 0 auto; } - /* Fade gradient at right edge */ + /* Remove fade gradient at right edge */ .scrollable-container:before { - content: ''; - position: absolute; - right: 0; - top: 0; - bottom: var(--spacing-md); - width: 40px; - background: linear-gradient(90deg, transparent, var(--background)); - z-index: 1; - pointer-events: none; + display: none; } - /* Arrow indicator for scroll */ + /* Remove arrow indicator for scroll */ .scrollable-container:after { - content: '→'; - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-50%); - color: var(--accent-color); - font-size: 20px; - opacity: 0.7; - z-index: 2; - pointer-events: none; - animation: pulse-hint 2s infinite ease-in-out; + display: none; } /* Scrollbar styling */ @@ -928,6 +872,12 @@ button:focus-visible { } } +/* 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 { @@ -1020,10 +970,6 @@ button:focus-visible { font-size: 20px; } - .category-title:after { - margin-left: var(--spacing-sm); - } - .resource-link { padding: var(--spacing-sm); } @@ -1084,17 +1030,6 @@ button:focus-visible { position: relative; } -.coming-next-content:before { - content: ''; - position: absolute; - left: 0; - top: 50%; - width: 100%; - height: 2px; - background: linear-gradient(90deg, var(--accent-color), transparent); - opacity: 0.2; -} - .coming-next-text { font-size: 18px; text-align: center; @@ -1153,11 +1088,6 @@ button:focus-visible { margin: 0; } -/* Alternating category backgrounds */ -.resource-category:nth-child(odd) { - background-color: rgba(255, 255, 255, 0.01); -} - /* Enhanced scrollable containers */ .scrollable-container { position: relative; @@ -1272,17 +1202,6 @@ button:focus-visible { position: relative; } -.coming-next-content:before { - content: ''; - position: absolute; - left: 0; - top: 50%; - width: 100%; - height: 2px; - background: linear-gradient(90deg, var(--accent-color), transparent); - opacity: 0.2; -} - /* Masonry-style grid for varying card heights */ @media (min-width: 768px) { .resource-list.masonry-grid { @@ -1352,3 +1271,57 @@ button:focus-visible { 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; +} diff --git a/web/main.dart b/web/main.dart index fdf5358..a1e573b 100644 --- a/web/main.dart +++ b/web/main.dart @@ -1,7 +1,74 @@ 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'); + 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); + })(); + ''' + ]); } From 18836b47e36aaf4c0951354f48cd3a921e8a72ba Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Mon, 12 May 2025 00:43:09 +0200 Subject: [PATCH 25/31] feat: enhance Navbar and ResourceLink components for improved usability and design * Implement mobile menu toggle functionality in Navbar, allowing users to easily access navigation links on smaller screens. * Refactor ResourceLink component to streamline the structure and improve accessibility by using URL directly instead of link attributes. * Update CSS styles for Navbar and resource cards, enhancing visual appeal and responsiveness across devices. * Ensure consistent layout and spacing for navigation and resource links, improving overall user experience. --- lib/src/components/navbar.dart | 93 +++-- lib/src/components/resource_category.dart | 177 +++++++++ lib/src/components/resource_link.dart | 253 ++----------- lib/src/pages/builders_page.dart | 62 +--- lib/src/pages/starters_page.dart | 103 ++---- web/css/styles.css | 415 +++++++++++++++++++++- web/main.dart | 10 + 7 files changed, 743 insertions(+), 370 deletions(-) create mode 100644 lib/src/components/resource_category.dart 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/resource_category.dart b/lib/src/components/resource_category.dart new file mode 100644 index 0000000..1a41cff --- /dev/null +++ b/lib/src/components/resource_category.dart @@ -0,0 +1,177 @@ +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; + + @override + Iterable build(BuildContext context) sync* { + final categoryClasses = ['resource-category']; + final String containerId = 'scrollable-${DateTime.now().millisecondsSinceEpoch}'; + + switch (accent) { + case CategoryAccent.none: + // No additional class needed + break; + case CategoryAccent.purple: + categoryClasses.add('purple-accent'); + break; + case CategoryAccent.blue: + categoryClasses.add('blue-accent'); + break; + case CategoryAccent.green: + categoryClasses.add('green-accent'); + break; + case CategoryAccent.orange: + categoryClasses.add('orange-accent'); + break; + } + + yield div( + classes: categoryClasses.join(' '), + attributes: { + 'role': 'region', + 'aria-label': title, + }, + [ + h3(classes: 'category-title', [text(title)]), + if (useHorizontalScroll) + div( + classes: 'scrollable-container dynamic-scroll-buttons', + attributes: { + 'id': containerId, + 'data-onrender': ''' + const container = document.getElementById('$containerId'); + if (container) { + const items = container.querySelector('.scrollable-items'); + const leftBtn = container.querySelector('.scroll-nav-left'); + const rightBtn = container.querySelector('.scroll-nav-right'); + + function checkOverflow() { + if (items && leftBtn && rightBtn) { + const hasOverflow = items.scrollWidth > items.clientWidth; + leftBtn.style.display = hasOverflow ? 'flex' : 'none'; + rightBtn.style.display = hasOverflow ? 'flex' : 'none'; + } + } + + // Check on load + checkOverflow(); + + // Check on resize + window.addEventListener('resize', checkOverflow); + + // Add event listeners for button clicks + if (leftBtn) { + leftBtn.addEventListener('click', () => { + items.scrollBy({ left: -300, behavior: 'smooth' }); + }); + } + + if (rightBtn) { + rightBtn.addEventListener('click', () => { + items.scrollBy({ left: 300, behavior: 'smooth' }); + }); + } + } + ''' + }, + [ + // Navigation buttons (visibility controlled by JS) + 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, + ), + ], + ) + else if (useGrid) + div( + classes: 'resource-grid', + resources, + ) + else + div( + classes: 'resource-list', + resources, + ), + ], + ); + + // If we're using horizontal scroll, also add a script to the page + // to look for the data-onrender attribute and execute it + if (useHorizontalScroll) { + yield script( + src: '', + attributes: {'type': 'text/javascript'}, + [ + text(''' + document.addEventListener('DOMContentLoaded', function() { + // Find all elements with data-onrender attribute + document.querySelectorAll('[data-onrender]').forEach(function(el) { + // Execute the script in data-onrender + try { + const scriptContent = el.getAttribute('data-onrender'); + if (scriptContent) { + new Function(scriptContent)(); + } + } catch (e) { + console.error('Error executing onrender script:', e); + } + }); + }); + '''), + ], + ); + } + } +} diff --git a/lib/src/components/resource_link.dart b/lib/src/components/resource_link.dart index 21b802b..c162037 100644 --- a/lib/src/components/resource_link.dart +++ b/lib/src/components/resource_link.dart @@ -1,36 +1,19 @@ import 'package:jaspr/browser.dart'; -enum CardVariant { - standard, - highlighted, - compact, - blueGlow, - greenGlow, - orangeGlow, -} - -enum CategoryAccent { - none, - purple, - blue, - green, - orange, -} - class ResourceLink extends StatelessComponent { const ResourceLink({ super.key, - required this.emoji, required this.title, - required this.link, - this.description, + required this.description, + required this.url, + this.emoji, this.variant = CardVariant.standard, }); - final String emoji; final String title; - final String link; - final String? description; + final String description; + final String url; + final String? emoji; final CardVariant variant; @override @@ -58,35 +41,32 @@ class ResourceLink extends StatelessComponent { break; } - yield li( - attributes: {'role': 'listitem'}, + yield a( + href: url, + target: Target.blank, + attributes: {'rel': 'noopener noreferrer'}, + classes: variantClasses.join(' '), [ - a( - href: link, - target: Target.blank, - classes: variantClasses.join(' '), - attributes: { - 'role': 'article', - 'aria-label': title + (description != null ? ' - $description' : ''), - 'rel': 'noopener noreferrer', - 'data-card-height': '${description != null ? (description!.length ~/ 2) + 120 : 80}', - }, + if (emoji != null) + span( + classes: 'resource-emoji', + attributes: {'aria-hidden': 'true'}, + [text(emoji!)], + ), + div( + classes: 'resource-content', [ - span(classes: 'resource-emoji', attributes: {'aria-hidden': 'true'}, [text(emoji)]), div( - classes: 'resource-content', + classes: 'resource-title', [ - span( - classes: 'resource-title', - [ - text(title), - ], - ), - if (description != null && variant != CardVariant.compact) - p( - classes: 'resource-description', - [text(description!)], - ), + if (emoji != null) text('$emoji '), + text(title), + ], + ), + div( + classes: 'resource-description', + [ + text(description), ], ), ], @@ -96,174 +76,11 @@ class ResourceLink extends StatelessComponent { } } -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; - - @override - Iterable build(BuildContext context) sync* { - final categoryClasses = ['resource-category']; - final String containerId = 'scrollable-${DateTime.now().millisecondsSinceEpoch}'; - - switch (accent) { - case CategoryAccent.none: - // No additional class needed - break; - case CategoryAccent.purple: - categoryClasses.add('purple-accent'); - break; - case CategoryAccent.blue: - categoryClasses.add('blue-accent'); - break; - case CategoryAccent.green: - categoryClasses.add('green-accent'); - break; - case CategoryAccent.orange: - categoryClasses.add('orange-accent'); - break; - } - - yield div( - classes: categoryClasses.join(' '), - attributes: { - 'role': 'region', - 'aria-label': title, - }, - [ - h3(classes: 'category-title', [text(title)]), - if (useHorizontalScroll) - div( - classes: 'scrollable-container dynamic-scroll-buttons', - attributes: { - 'id': containerId, - // Add the check overflow logic as an onMount script attribute - 'data-onrender': ''' - const container = document.getElementById('$containerId'); - if (container) { - const items = container.querySelector('.scrollable-items'); - const leftBtn = container.querySelector('.scroll-nav-left'); - const rightBtn = container.querySelector('.scroll-nav-right'); - - function checkOverflow() { - if (items && leftBtn && rightBtn) { - const hasOverflow = items.scrollWidth > items.clientWidth; - leftBtn.style.display = hasOverflow ? 'flex' : 'none'; - rightBtn.style.display = hasOverflow ? 'flex' : 'none'; - } - } - - // Check on load - checkOverflow(); - - // Check on resize - window.addEventListener('resize', checkOverflow); - - // Add event listeners for button clicks - if (leftBtn) { - leftBtn.addEventListener('click', () => { - items.scrollBy({ left: -300, behavior: 'smooth' }); - }); - } - - if (rightBtn) { - rightBtn.addEventListener('click', () => { - items.scrollBy({ left: 300, behavior: 'smooth' }); - }); - } - } - ''' - }, - [ - // Navigation buttons (visibility controlled by JS) - div( - classes: 'scroll-nav-button scroll-nav-left', - attributes: { - 'aria-label': 'Scroll left', - 'style': - 'display: none; background-color: var(--primary-color); color: white;', // Hidden by default, improved contrast - '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;', // Hidden by default, improved contrast - 'title': 'Scroll to see more items', - }, - [text('→')], - ), - div( - classes: 'scrollable-items', - resources, - ), - ], - ) - else - ul( - classes: [ - 'resource-list', - if (useGrid) 'grid-layout', - if (useMasonryGrid) 'masonry-grid', - ].where((c) => c.isNotEmpty).join(' '), - attributes: { - 'role': 'list', - if (useMasonryGrid) - 'data-onrender': ''' - document.querySelectorAll('.masonry-grid .resource-link').forEach(card => { - const height = card.getAttribute('data-card-height'); - if (height) { - card.style.setProperty('--card-height', height); - } - }); - ''', - }, - resources, - ), - ], - ); - - // If we're using horizontal scroll or masonry grid, also add a script to the page - // to look for the data-onrender attribute and execute it - if (useHorizontalScroll || useMasonryGrid) { - yield script( - src: '', - attributes: {'type': 'text/javascript'}, - [ - text(''' - document.addEventListener('DOMContentLoaded', function() { - // Find all elements with data-onrender attribute - document.querySelectorAll('[data-onrender]').forEach(function(el) { - // Execute the script in data-onrender - try { - const scriptContent = el.getAttribute('data-onrender'); - if (scriptContent) { - new Function(scriptContent)(); - } - } catch (e) { - console.error('Error executing onrender script:', e); - } - }); - }); - '''), - ], - ); - } - } +enum CardVariant { + standard, + highlighted, + compact, + blueGlow, + greenGlow, + orangeGlow, } diff --git a/lib/src/pages/builders_page.dart b/lib/src/pages/builders_page.dart index ecd8ed9..12aefa9 100644 --- a/lib/src/pages/builders_page.dart +++ b/lib/src/pages/builders_page.dart @@ -2,6 +2,7 @@ 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'; @@ -91,34 +92,27 @@ class BuildersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '📚', title: 'Accessible Resources', description: 'Create clear documentation, tutorials, and examples for Flutter developers to incorporate AI into their apps', - link: ExternalLink.forumCategory.url, - variant: CardVariant.highlighted, + url: ExternalLink.forumCategory.url, ), ResourceLink( - emoji: '🧪', title: 'Best Practices', description: 'Explore and share responsible AI implementation patterns with the community', - link: ExternalLink.youTubeAgenticQA.url, - variant: CardVariant.blueGlow, + url: ExternalLink.youTubeAgenticQA.url, ), ResourceLink( - emoji: '🤝', title: 'Supportive Environment', description: 'Build a welcoming community for experimentation, learning, and collaboration', - link: ExternalLink.forumChat.url, + url: ExternalLink.forumChat.url, ), ResourceLink( - emoji: '📦', title: 'Open Source', description: 'Develop starter kits, reference implementations, and shared resources', - link: ExternalLink.socialGitHub.url, - variant: CardVariant.orangeGlow, + url: ExternalLink.socialGitHub.url, ), ], ), @@ -173,25 +167,19 @@ class BuildersPage extends StatelessComponent { title: 'YouTube Resources', resources: [ ResourceLink( - emoji: '🎬', title: 'AI Circle Playlist', description: 'Collection of our community sessions and tutorials', - link: ExternalLink.youTubePlaylist.url, - variant: CardVariant.highlighted, + url: ExternalLink.youTubePlaylist.url, ), ResourceLink( - emoji: '🎮', title: 'Vibe Coding Sessions', description: 'Relaxed coding streams building AI-powered apps', - link: ExternalLink.youTubeVibeCoding.url, - variant: CardVariant.greenGlow, + url: ExternalLink.youTubeVibeCoding.url, ), ResourceLink( - emoji: '🧠', title: 'Agentic Q&A Session', description: 'Deep dive into agentic app patterns', - link: ExternalLink.youTubeAgenticQA.url, - variant: CardVariant.orangeGlow, + url: ExternalLink.youTubeAgenticQA.url, ), ], ), @@ -214,19 +202,15 @@ class BuildersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '🔄', title: 'Function Calling with Gemini', description: 'Official documentation for implementing function calling with Gemini AI', - link: ExternalLink.geminiFunctionCallingDocs.url, - variant: CardVariant.highlighted, + url: ExternalLink.geminiFunctionCallingDocs.url, ), ResourceLink( - emoji: '🎥', title: 'MCP for Flutter Developers', description: 'Understanding the Model Context Protocol for Flutter applications', - link: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, - variant: CardVariant.blueGlow, + url: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, ), ], ), @@ -236,25 +220,19 @@ class BuildersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '📺', title: 'YouTube Channel', description: 'Tutorials, livestreams, and coding sessions from our community', - link: ExternalLink.youTubeFlutterCommunity.url, - variant: CardVariant.highlighted, + url: ExternalLink.youTubeFlutterCommunity.url, ), ResourceLink( - emoji: '💬', title: 'Flutter Forum', description: 'Discussions, Q&A, and knowledge sharing about Flutter and AI', - link: ExternalLink.forumCategory.url, - variant: CardVariant.greenGlow, + url: ExternalLink.forumCategory.url, ), ResourceLink( - emoji: '📝', title: 'Community Survey', description: 'Help shape the future of our community by sharing your feedback', - link: ExternalLink.surveyCommunity.url, - variant: CardVariant.orangeGlow, + url: ExternalLink.surveyCommunity.url, ), ], ), @@ -275,30 +253,24 @@ class BuildersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '📝', title: 'Write Documentation', description: 'Help create clear guides, examples, and tutorials for the community', - link: ExternalLink.socialGitHub.url, - variant: CardVariant.blueGlow, + url: ExternalLink.socialGitHub.url, ), ResourceLink( - emoji: '🧪', title: 'Create Example Apps', description: 'Build sample projects demonstrating AI integration patterns', - link: ExternalLink.socialGitHub.url, - variant: CardVariant.highlighted, + url: ExternalLink.socialGitHub.url, ), ResourceLink( - emoji: '🔍', title: 'Review Code', description: 'Help ensure quality and consistency in our code samples', - link: ExternalLink.socialGitHub.url, + url: ExternalLink.socialGitHub.url, ), ResourceLink( - emoji: '🌐', title: 'Spread the Word', description: 'Share our resources with your network and community', - link: ExternalLink.surveyCommunity.url, + url: ExternalLink.surveyCommunity.url, ), ], ), diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index 2601099..08640f0 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -1,6 +1,7 @@ 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'; @@ -65,43 +66,35 @@ class StartersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '🧠', title: 'LangChain: Introduction', description: 'Learn the foundations of LangChain and its modular architecture for building LLM-powered agents.', - link: 'https://python.langchain.com/docs/introduction/', - variant: CardVariant.standard, + url: 'https://python.langchain.com/docs/introduction/', ), ResourceLink( - emoji: '📘', title: 'What is Agentic AI? (IBM)', description: 'Overview of how agentic AI differs from traditional AI, with real-world examples.', - link: + url: 'https://www.ibm.com/think/topics/agentic-ai#:~:text=Agentic%20AI%20solutions%20can%20be,predictive%20analytics%20and%20execute%20trades.', ), ResourceLink( - emoji: '🔍', title: 'Introducing the Model Context Protocol (Anthropic)', description: 'Learn how MCP standardizes interactions between LLMs and tools.', - link: 'https://www.anthropic.com/news/model-context-protocol', - variant: CardVariant.standard, + url: 'https://www.anthropic.com/news/model-context-protocol', ), ResourceLink( - emoji: '📚', title: 'Amazon AWS: What is RAG (Retrieval-Augmented Generation)?', description: 'A simple primer on Retrieval-Augmented Generation and how it improves model accuracy.', - link: 'https://aws.amazon.com/what-is/retrieval-augmented-generation/', + url: 'https://aws.amazon.com/what-is/retrieval-augmented-generation/', ), ResourceLink( - emoji: '🎥', title: 'Observable Flutter #59 – Agentic Apps with Flutter', description: 'Craig Labenz and Andrew Brogdon walk through agentic patterns, Colorist, and Firebase Genkit.', - link: + url: 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', - variant: CardVariant.standard, ), ], ), @@ -120,40 +113,34 @@ class StartersPage extends StatelessComponent { accent: CategoryAccent.purple, resources: [ ResourceLink( - emoji: '🎨', title: 'Gemini API + Flutter (Colorist Codelab)', description: 'Beginner/Intermediate. Learn how to integrate Gemini into a Flutter app using Firebase extensions and function calling.', - link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', - variant: CardVariant.standard, + url: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', ), ResourceLink( - emoji: '🤖', title: 'OpenAI API Docs', description: 'Beginner. Covers completions, chat, and function calling for GPT models.', - link: 'https://platform.openai.com/docs', + url: 'https://platform.openai.com/docs', ), ResourceLink( - emoji: '🧠', title: 'Anthropic Claude API', description: 'Beginner. Official Claude API docs with example request/response flows.', - link: 'https://docs.anthropic.com/claude/docs', + url: 'https://docs.anthropic.com/claude/docs', ), ResourceLink( - emoji: '📦', title: 'mcp_dart (Community SDK)', description: 'Integrate LLM applications with external data and tools using this Dart SDK for the Model Context Protocol (MCP).', - link: 'https://pub.dev/packages/mcp_dart', + url: 'https://pub.dev/packages/mcp_dart', ), ResourceLink( - emoji: '📦', title: 'dart_openai (Community SDK)', description: 'Intermediate. A Dart package that simplifies using OpenAI APIs within Flutter projects.', - link: 'https://pub.dev/packages/dart_openai', + url: 'https://pub.dev/packages/dart_openai', ), ], ), @@ -162,19 +149,16 @@ class StartersPage extends StatelessComponent { accent: CategoryAccent.green, resources: [ ResourceLink( - emoji: '🎤', title: 'OpenAI Whisper API', description: 'Beginner. Transcribe audio to text using OpenAI\'s hosted Whisper model.', - link: 'https://platform.openai.com/docs/guides/speech-to-text', - variant: CardVariant.standard, + url: 'https://platform.openai.com/docs/guides/speech-to-text', ), ResourceLink( - emoji: '📱', title: 'flutter_whisper.cpp', description: 'Advanced. Community-maintained Flutter app running Whisper on-device via Rust FFI and whisper.cpp.', - link: 'https://github.com/lyledean1/flutter_whisper.cpp', + url: 'https://github.com/lyledean1/flutter_whisper.cpp', ), ], ), @@ -184,20 +168,16 @@ class StartersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '🔍', title: 'Firestore Vector Search (Extension)', description: 'Intermediate. Store and retrieve vector embeddings in Firestore for building RAG pipelines.', - link: 'https://extensions.dev/extensions/googlecloud/firestore-vector-search', - variant: CardVariant.standard, + url: 'https://extensions.dev/extensions/googlecloud/firestore-vector-search', ), ResourceLink( - emoji: '⚡', title: 'Firebase Genkit', description: 'Advanced. A backend framework for building AI-driven workflows with Gemini, designed for Cloud Functions.', - link: 'https://firebase.google.com/docs/genkit', - variant: CardVariant.standard, + url: 'https://firebase.google.com/docs/genkit', ), ], ), @@ -206,12 +186,10 @@ class StartersPage extends StatelessComponent { accent: CategoryAccent.blue, resources: [ ResourceLink( - emoji: '🔄', title: 'Model Context Protocol Overview', description: 'Advanced. MCP is an open protocol to connect LLMs with tools and data providers.', - link: 'https://modelcontextprotocol.io/introduction', - variant: CardVariant.blueGlow, + url: 'https://modelcontextprotocol.io/introduction', ), ], ), @@ -219,11 +197,10 @@ class StartersPage extends StatelessComponent { title: 'Local LLMs (Ollama)', resources: [ ResourceLink( - emoji: '🏠', title: 'Ollama', description: 'Intermediate. Easily run LLMs like LLaMA or Mistral locally and serve them via HTTP.', - link: 'https://github.com/ollama/ollama', + url: 'https://github.com/ollama/ollama', ), ], ), @@ -233,18 +210,15 @@ class StartersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '🔧', title: 'Function Calling (OpenAI)', description: 'Intermediate. Enables models to call structured functions from your app.', - link: 'https://platform.openai.com/docs/guides/function-calling', + url: 'https://platform.openai.com/docs/guides/function-calling', ), ResourceLink( - emoji: '🤖', title: 'LLM Agents (Prompt Engineering Guide)', description: 'Advanced. Explore ReAct and other reasoning/action agent patterns.', - link: 'https://www.promptingguide.ai/techniques/react', - variant: CardVariant.highlighted, + url: 'https://www.promptingguide.ai/techniques/react', ), ], ), @@ -254,34 +228,28 @@ class StartersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '🎨', title: 'Colorist (Gemini + Flutter)', description: 'Intermediate. Gemini-powered Flutter app that updates the UI based on natural language prompts.', - link: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', - variant: CardVariant.standard, + url: 'https://codelabs.developers.google.com/codelabs/flutter-gemini-colorist', ), ResourceLink( - emoji: '📚', title: 'Gemini API Cookbook (GitHub)', description: 'Intermediate/Advanced. Google\'s official examples for calling Gemini API across languages including Dart.', - link: 'https://github.com/google-gemini/cookbook', - variant: CardVariant.standard, + url: 'https://github.com/google-gemini/cookbook', ), ResourceLink( - emoji: '🚀', title: 'Gemini Flutter Weather App', description: 'Beginner/Intermediate. Weather app powered by Gemini AI with natural language interaction.', - link: 'https://github.com/flutter/samples/tree/main/ai_weather', + url: 'https://github.com/flutter/samples/tree/main/ai_weather', ), ResourceLink( - emoji: '📝', title: 'OpenAI Translator Example', description: 'Intermediate. Demonstrates using OpenAI to build a multilingual translator app.', - link: + url: 'https://github.com/openai/openai-cookbook/blob/main/examples/How_to_build_a_multilingual_translator.md', ), ], @@ -290,27 +258,22 @@ class StartersPage extends StatelessComponent { title: 'Dart AI Libraries', resources: [ ResourceLink( - emoji: '🔗', title: 'LangChain.dart', description: 'Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more.', - link: 'https://pub.dev/packages/langchain', - variant: CardVariant.standard, + url: 'https://pub.dev/packages/langchain', ), ResourceLink( - emoji: '🤖', title: 'Flutter GenAI', description: 'Advanced. A collection of Flutter plugins for integrating generative AI capabilities into your applications.', - link: 'https://pub.dev/packages/flutter_genai', - variant: CardVariant.standard, + url: 'https://pub.dev/packages/flutter_genai', ), ResourceLink( - emoji: '📱', title: 'Dart OpenAI', description: 'Beginner/Intermediate. A Dart client for OpenAI API with chat, completions, embeddings and more.', - link: 'https://pub.dev/packages/dart_openai', + url: 'https://pub.dev/packages/dart_openai', ), ], ), @@ -329,29 +292,23 @@ class StartersPage extends StatelessComponent { useGrid: true, resources: [ ResourceLink( - emoji: '🎥', title: 'Agentic Apps with Flutter (Observable Flutter #59)', - link: - 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', description: 'Craig Labenz and Andrew Brogdon discuss and explore AI-powered, agentic apps in Flutter in this Observable Flutter episode.', - variant: CardVariant.standard, + url: + 'https://www.youtube.com/watch?v=eczLRG3q4V0&list=PLjxrf2q8roU1GHtc2FCHoEZr_v-LqnTZX', ), ResourceLink( - emoji: '💬', title: 'Gemini API: Function Calling Docs', - link: ExternalLink.geminiFunctionCallingDocs.url, 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.', - variant: CardVariant.standard, + url: ExternalLink.geminiFunctionCallingDocs.url, ), ResourceLink( - emoji: '🎥', 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).', - link: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, - variant: CardVariant.standard, + url: ExternalLink.youTubeJhinLeeMCPHumpdayQA.url, ), ], ), diff --git a/web/css/styles.css b/web/css/styles.css index 853af88..358e768 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -33,6 +33,9 @@ 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 { @@ -345,7 +348,7 @@ footer a:hover, a.selected { } } -/* Navbar styles */ +/* Updated Navbar styles */ .navbar { position: fixed; top: 0; @@ -353,30 +356,63 @@ 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); + width: 100%; + border-bottom: 1px solid var(--border-color); } .navbar .container { display: flex; + flex-wrap: wrap; justify-content: space-between; align-items: center; + width: 100%; + 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 { @@ -390,14 +426,249 @@ 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; + width: 100%; + 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 { + width: 100%; + 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; @@ -659,7 +930,7 @@ button:focus-visible { .resource-title { font-weight: 600; - color: var(--accent-color); + color: var(--heading-color); display: flex; align-items: center; gap: 8px; @@ -724,7 +995,7 @@ button:focus-visible { /* Grid layout for resource lists */ .resource-list.grid-layout { display: grid; - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: var(--spacing-md); } @@ -1325,3 +1596,135 @@ button:focus-visible { 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 { + width: 100%; + 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 { + overflow-x: hidden; + 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; + width: 100%; +} + +/* 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/main.dart b/web/main.dart index a1e573b..3351c28 100644 --- a/web/main.dart +++ b/web/main.dart @@ -28,6 +28,16 @@ void main() { 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; From efba8449373799cd082184891b9cf489315d4056 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Mon, 12 May 2025 00:50:34 +0200 Subject: [PATCH 26/31] feat: update ResourceCategory and ResourceLink components for improved structure and functionality * Refactor ResourceCategory to utilize helper methods for rendering scrollable containers, resource lists, and grids, enhancing code readability and maintainability. * Introduce accent class handling in ResourceCategory for better visual differentiation based on category type. * Enhance ResourceLink by implementing a variant class system, allowing for more flexible styling options. * Update website.dart to include new JavaScript files for improved functionality, ensuring better user experience with dynamic content. --- lib/src/app/website.dart | 4 +- lib/src/components/resource_category.dart | 182 ++++++++-------------- lib/src/components/resource_link.dart | 50 +++--- 3 files changed, 93 insertions(+), 143 deletions(-) diff --git a/lib/src/app/website.dart b/lib/src/app/website.dart index 9f5caf7..a244a43 100644 --- a/lib/src/app/website.dart +++ b/lib/src/app/website.dart @@ -21,8 +21,8 @@ class _SiteState extends State { link(href: '/css/fontawesome.css', rel: 'stylesheet'), link(href: '/css/brands.css', rel: 'stylesheet'), link(href: '/css/clickable-cards.css', rel: 'stylesheet'), - // script(src: '/js/clickable-cards.js', attributes: {'defer': 'true'}, []), // Commented out - // Style(styles: resetStyles) + script(src: '/js/scroll.js', attributes: {'defer': 'true'}, []), + script(src: '/js/clickable-cards.js', attributes: {'defer': 'true'}, []), ], ); yield Router( diff --git a/lib/src/components/resource_category.dart b/lib/src/components/resource_category.dart index 1a41cff..bd766ab 100644 --- a/lib/src/components/resource_category.dart +++ b/lib/src/components/resource_category.dart @@ -36,27 +36,75 @@ class ResourceCategory extends StatelessComponent { final bool useHorizontalScroll; final bool useMasonryGrid; - @override - Iterable build(BuildContext context) sync* { - final categoryClasses = ['resource-category']; - final String containerId = 'scrollable-${DateTime.now().millisecondsSinceEpoch}'; - + String get accentClassName { switch (accent) { case CategoryAccent.none: - // No additional class needed - break; + return ''; case CategoryAccent.purple: - categoryClasses.add('purple-accent'); - break; + return 'purple-accent'; case CategoryAccent.blue: - categoryClasses.add('blue-accent'); - break; + return 'blue-accent'; case CategoryAccent.green: - categoryClasses.add('green-accent'); - break; + return 'green-accent'; case CategoryAccent.orange: - categoryClasses.add('orange-accent'); - break; + 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( @@ -68,110 +116,12 @@ class ResourceCategory extends StatelessComponent { [ h3(classes: 'category-title', [text(title)]), if (useHorizontalScroll) - div( - classes: 'scrollable-container dynamic-scroll-buttons', - attributes: { - 'id': containerId, - 'data-onrender': ''' - const container = document.getElementById('$containerId'); - if (container) { - const items = container.querySelector('.scrollable-items'); - const leftBtn = container.querySelector('.scroll-nav-left'); - const rightBtn = container.querySelector('.scroll-nav-right'); - - function checkOverflow() { - if (items && leftBtn && rightBtn) { - const hasOverflow = items.scrollWidth > items.clientWidth; - leftBtn.style.display = hasOverflow ? 'flex' : 'none'; - rightBtn.style.display = hasOverflow ? 'flex' : 'none'; - } - } - - // Check on load - checkOverflow(); - - // Check on resize - window.addEventListener('resize', checkOverflow); - - // Add event listeners for button clicks - if (leftBtn) { - leftBtn.addEventListener('click', () => { - items.scrollBy({ left: -300, behavior: 'smooth' }); - }); - } - - if (rightBtn) { - rightBtn.addEventListener('click', () => { - items.scrollBy({ left: 300, behavior: 'smooth' }); - }); - } - } - ''' - }, - [ - // Navigation buttons (visibility controlled by JS) - 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, - ), - ], - ) + renderScrollableContainer(containerId) else if (useGrid) - div( - classes: 'resource-grid', - resources, - ) + renderResourceGrid() else - div( - classes: 'resource-list', - resources, - ), + renderResourceList(), ], ); - - // If we're using horizontal scroll, also add a script to the page - // to look for the data-onrender attribute and execute it - if (useHorizontalScroll) { - yield script( - src: '', - attributes: {'type': 'text/javascript'}, - [ - text(''' - document.addEventListener('DOMContentLoaded', function() { - // Find all elements with data-onrender attribute - document.querySelectorAll('[data-onrender]').forEach(function(el) { - // Execute the script in data-onrender - try { - const scriptContent = el.getAttribute('data-onrender'); - if (scriptContent) { - new Function(scriptContent)(); - } - } catch (e) { - console.error('Error executing onrender script:', e); - } - }); - }); - '''), - ], - ); - } } } diff --git a/lib/src/components/resource_link.dart b/lib/src/components/resource_link.dart index c162037..055c78f 100644 --- a/lib/src/components/resource_link.dart +++ b/lib/src/components/resource_link.dart @@ -1,5 +1,14 @@ import 'package:jaspr/browser.dart'; +enum CardVariant { + standard, + highlighted, + compact, + blueGlow, + greenGlow, + orangeGlow, +} + class ResourceLink extends StatelessComponent { const ResourceLink({ super.key, @@ -16,29 +25,29 @@ class ResourceLink extends StatelessComponent { final String? emoji; final CardVariant variant; - @override - Iterable build(BuildContext context) sync* { - final variantClasses = ['resource-link']; - + String get variantClassName { switch (variant) { case CardVariant.standard: - // No additional class needed - break; + return ''; case CardVariant.highlighted: - variantClasses.add('highlighted'); - break; + return 'highlighted'; case CardVariant.compact: - variantClasses.add('compact'); - break; + return 'compact'; case CardVariant.blueGlow: - variantClasses.add('blue-glow'); - break; + return 'blue-glow'; case CardVariant.greenGlow: - variantClasses.add('green-glow'); - break; + return 'green-glow'; case CardVariant.orangeGlow: - variantClasses.add('orange-glow'); - break; + return 'orange-glow'; + } + } + + @override + Iterable build(BuildContext context) sync* { + final variantClasses = ['resource-link']; + + if (variantClassName.isNotEmpty) { + variantClasses.add(variantClassName); } yield a( @@ -75,12 +84,3 @@ class ResourceLink extends StatelessComponent { ); } } - -enum CardVariant { - standard, - highlighted, - compact, - blueGlow, - greenGlow, - orangeGlow, -} From b99376109f1a51b00651a0f9949f48854a610a82 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Mon, 12 May 2025 00:53:08 +0200 Subject: [PATCH 27/31] chore: remove outdated README files from builders and starters sections * Delete README.md files from the builders and starters directories, as they contained outdated information and are no longer relevant to the project. * This cleanup helps streamline documentation and maintain focus on current resources and guidelines. --- docs/builders/README.md | 37 ------------- docs/starters/README.md | 113 ---------------------------------------- 2 files changed, 150 deletions(-) delete mode 100644 docs/builders/README.md delete mode 100644 docs/starters/README.md 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 From ce41bb557a3ad123d2eb65467843545e96fa0c79 Mon Sep 17 00:00:00 2001 From: Esra Kadah <87121319+esrakadah@users.noreply.github.com> Date: Sun, 15 Jun 2025 17:53:50 +0200 Subject: [PATCH 28/31] chore: update project configuration and dependencies * Add .fvmrc file to specify Flutter version 3.29.3 for consistent development environment. * Update Dart SDK constraints in pubspec.yaml to allow versions from 3.3.0 to less than 4.0.0. * Modify pubspec.lock to reflect Dart SDK version change. * Enhance VSCode launch configurations for improved development experience with new options for running with webdev. * Introduce settings.json and tasks.json for better integration with VSCode, including a task for webdev serve with auto-refresh capability. --- .fvmrc | 3 +++ .vscode/launch.json | 15 ++++++++++++++- .vscode/settings.json | 3 +++ .vscode/tasks.json | 25 +++++++++++++++++++++++++ pubspec.lock | 2 +- pubspec.yaml | 2 +- 6 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 .fvmrc create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json 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 index 9eb25d6..f84cafd 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,10 +5,23 @@ "version": "0.2.0", "configurations": [ { - "name": "ai_builders_circle", + "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/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 From d66d00005ff284db91a1a0f908c0a3da8875dc89 Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Sun, 15 Jun 2025 20:37:22 -0400 Subject: [PATCH 29/31] Fix responsiveness --- web/css/styles.css | 258 +++++++++++++++++++++++++-------------------- 1 file changed, 146 insertions(+), 112 deletions(-) diff --git a/web/css/styles.css b/web/css/styles.css index 358e768..b628011 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; @@ -39,7 +40,7 @@ body > * { } body { - font-family: 'Inter', sans-serif; + font-family: "Inter", sans-serif; background-color: var(--background); color: var(--text-color); line-height: 1.5; @@ -80,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); } @@ -105,9 +108,7 @@ a:hover { transform: translateY(-2px); } - .page-layout { - width: 100%; margin-top: var(--navbar-height); } @@ -138,7 +139,7 @@ a:hover { transparent 70% ); /* Remove animation that causes visual noise */ - /* animation: pulse-bg 10s infinite alternate ease-in-out; */ +/* animation: pulse-bg 10s infinite alternate ease-in-out; */ /* } */ @@ -180,7 +181,8 @@ a:hover { 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 */ +.hero .button { + /* This rule seems to style text within a .button class, not the container */ font-size: 18px; } @@ -307,7 +309,8 @@ footer a { transition: color var(--transition-speed); } -footer a:hover, a.selected { +footer a:hover, +a.selected { color: var(--accent-color); } @@ -358,7 +361,6 @@ footer a:hover, a.selected { padding: 20px 0; background-color: var(--bg-color); backdrop-filter: blur(10px); - width: 100%; border-bottom: 1px solid var(--border-color); } @@ -367,7 +369,6 @@ footer a:hover, a.selected { flex-wrap: wrap; justify-content: space-between; align-items: center; - width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 var(--spacing-md); @@ -416,7 +417,7 @@ footer a:hover, a.selected { } .nav-link::after { - content: ''; + content: ""; position: absolute; width: 0; height: 2px; @@ -426,7 +427,7 @@ 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%; } @@ -454,7 +455,6 @@ footer a:hover, a.selected { .mobile-menu-toggle span { display: block; height: 3px; - width: 100%; background-color: var(--text-color); border-radius: 3px; transition: all 0.3s ease; @@ -561,76 +561,75 @@ footer a:hover, a.selected { 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 { - width: 100%; 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, + .nav-link, .footer a, .resource-link { padding: 10px 0; } - + /* Add extra space around interactive elements */ .cta_button, .secondary-button, @@ -640,26 +639,26 @@ footer a:hover, a.selected { 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; @@ -1078,44 +1077,44 @@ button:focus-visible { 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; @@ -1129,13 +1128,13 @@ button:focus-visible { 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; @@ -1164,9 +1163,15 @@ button:focus-visible { /* Pulse animation */ @keyframes pulse { - 0% { opacity: 0.7; } - 50% { opacity: 0.3; } - 100% { opacity: 0.7; } + 0% { + opacity: 0.7; + } + 50% { + opacity: 0.3; + } + 100% { + opacity: 0.7; + } } /* Navigation buttons for horizontal scroll - now controlled by JavaScript for visibility */ @@ -1186,7 +1191,9 @@ button:focus-visible { border: 1px solid rgba(171, 87, 255, 0.2); cursor: pointer; z-index: 10; - transition: opacity 0.3s ease, background-color 0.3s ease; + transition: + opacity 0.3s ease, + background-color 0.3s ease; opacity: 0; } @@ -1228,7 +1235,7 @@ button:focus-visible { .resource-list.grid-layout { grid-template-columns: 1fr; } - + .resource-category.purple-accent, .resource-category.blue-accent, .resource-category.green-accent, @@ -1236,15 +1243,15 @@ button:focus-visible { 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; @@ -1316,7 +1323,7 @@ button:focus-visible { } .highlight-link:after { - content: ''; + content: ""; position: absolute; bottom: -2px; left: 0; @@ -1366,7 +1373,7 @@ button:focus-visible { .scrollable-container:before, .scrollable-container:after { - content: ''; + content: ""; position: absolute; top: 0; bottom: 0; @@ -1392,7 +1399,9 @@ button:focus-visible { background-color: rgba(171, 87, 255, 0.15); border-radius: 50%; opacity: 0; - transition: opacity 0.3s ease, background-color 0.3s ease; + transition: + opacity 0.3s ease, + background-color 0.3s ease; z-index: 10; } @@ -1408,7 +1417,7 @@ button:focus-visible { /* Mobile swipe indicator */ @media (max-width: 768px) { .scrollable-container:after { - content: '→'; + content: "→"; background: none; display: flex; align-items: center; @@ -1418,11 +1427,20 @@ button:focus-visible { 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; } + 0% { + transform: translateX(0); + opacity: 0.7; + } + 50% { + transform: translateX(5px); + opacity: 1; + } + 100% { + transform: translateX(0); + opacity: 0.7; + } } } @@ -1446,7 +1464,7 @@ button:focus-visible { } .cta_button:before { - content: ''; + content: ""; position: absolute; top: -50%; left: -50%; @@ -1459,8 +1477,13 @@ button:focus-visible { } @keyframes button-shine { - 0% { transform: translateX(-100%) rotate(45deg); } - 30%, 100% { transform: translateX(100%) rotate(45deg); } + 0% { + transform: translateX(-100%) rotate(45deg); + } + 30%, + 100% { + transform: translateX(100%) rotate(45deg); + } } .cta_button span { @@ -1511,7 +1534,7 @@ button:focus-visible { } .content-card.animate-cta .cta_button:before { - content: ''; + content: ""; position: absolute; top: -50%; left: -50%; @@ -1524,8 +1547,13 @@ button:focus-visible { } @keyframes button-shine { - 0% { transform: translateX(-100%) rotate(45deg); } - 30%, 100% { transform: translateX(100%) rotate(45deg); } + 0% { + transform: translateX(-100%) rotate(45deg); + } + 30%, + 100% { + transform: translateX(100%) rotate(45deg); + } } .content-card.animate-cta .cta_button span { @@ -1603,67 +1631,67 @@ button:focus-visible { .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, + + .cta_button, .secondary-button { - width: 100%; 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 { + html, + body { overflow-x: hidden; width: 100%; } @@ -1675,56 +1703,62 @@ button:focus-visible { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--spacing-lg); margin: 1.5rem 0; - width: 100%; } /* Animations */ @keyframes pulse-bg { - 0% { opacity: 0.5; transform: scale(1); } - 100% { opacity: 0.8; transform: scale(1.1); } + 0% { + opacity: 0.5; + transform: scale(1); + } + 100% { + opacity: 0.8; + transform: scale(1.1); + } } /* Button animations */ .cta_button { - position: relative; - overflow: hidden; + 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; + 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); } + 0% { + transform: translateX(-100%) rotate(45deg); + } + 30%, + 100% { + transform: translateX(100%) rotate(45deg); + } } .cta_button span { - position: relative; - z-index: 2; + 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; + 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; } From 1caa9d8014f04a325b0d6e5f908f8214f4a3153e Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Sun, 15 Jun 2025 20:37:35 -0400 Subject: [PATCH 30/31] Remove broken link --- lib/src/pages/starters_page.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/src/pages/starters_page.dart b/lib/src/pages/starters_page.dart index 08640f0..b218c31 100644 --- a/lib/src/pages/starters_page.dart +++ b/lib/src/pages/starters_page.dart @@ -263,12 +263,6 @@ class StartersPage extends StatelessComponent { 'Intermediate. Community Dart port of LangChain, featuring clients for OpenAI, Google, Anthropic, and more.', url: 'https://pub.dev/packages/langchain', ), - ResourceLink( - title: 'Flutter GenAI', - description: - 'Advanced. A collection of Flutter plugins for integrating generative AI capabilities into your applications.', - url: 'https://pub.dev/packages/flutter_genai', - ), ResourceLink( title: 'Dart OpenAI', description: From 7e7ca19edc7b97f6d22af1284e03af7dc5c5ea8c Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Sun, 15 Jun 2025 20:45:33 -0400 Subject: [PATCH 31/31] Remove weird scrollbar on mobile view --- web/css/styles.css | 1 - 1 file changed, 1 deletion(-) diff --git a/web/css/styles.css b/web/css/styles.css index b628011..b7bef86 100644 --- a/web/css/styles.css +++ b/web/css/styles.css @@ -1692,7 +1692,6 @@ button:focus-visible { /* Fix any potential horizontal overflow issues */ html, body { - overflow-x: hidden; width: 100%; } }