added activities and conference type, get list dynamic, todo: year, month params and fix ui for list whit images.

This commit is contained in:
Esteban Paz 2025-08-03 12:23:47 -05:00
parent 19d2968b8c
commit e9f834c267
12 changed files with 424 additions and 210 deletions

View File

@ -1,19 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:lgcc/pages/conferences.dart';
import 'package:lgcc/pages/home.dart';
import 'package:lgcc/navigation_bar.dart';
// import 'package:lgcc/pages/items_list.dart';
Future main() async {
await dotenv.load(fileName: ".env");
runApp(MyApp());
}
class MyApp extends StatelessWidget {
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: ConferencesPage(),
return MaterialApp(
home: MainNavigationBar(),
);
}
}

View File

@ -1,98 +0,0 @@
class TranslationModel {
final int id;
final String language;
final String title;
final String? markdown;
TranslationModel({
required this.id,
required this.language,
required this.title,
this.markdown,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'language': language,
'title': title,
'markdown': markdown,
};
}
factory TranslationModel.fromJson(Map<String, dynamic> json) {
return TranslationModel(
id: json['id'] ?? 0,
language: json['language'] ?? '',
title: json['title'] ?? '',
markdown: json['markdown'] ?? '',
);
}
}
class ConferencesModel {
final int id;
final String title;
final DateTime date;
int activity;
int duration;
String place;
String city;
String state;
String country;
// List<String> type;
String thumbnail;
List<TranslationModel> translations;
ConferencesModel({
required this.id,
required this.title,
required this.date,
this.activity = 0,
this.duration = 0,
this.place = '',
this.city = '',
this.state = '',
this.country = '',
// this.type = const [],
this.thumbnail = '',
this.translations = const [],
});
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'date': date.toIso8601String(),
'activity': activity,
'duration': duration,
'place': place,
'city': city,
'state': state,
'country': country,
// 'type': type,
'thumbnail': thumbnail.toString(),
'translations': translations.toString() ,
};
}
factory ConferencesModel.fromJson(Map<String, dynamic> json) {
return ConferencesModel(
id: json['id'] ?? 0,
title: json['title'] ?? '',
date: json['date'] != null ? DateTime.parse(json['date'].toString()) : DateTime.now(),
activity: json['activity'] ?? 0,
duration: json['duration'] ?? 0,
place: json['place'] ?? '',
city: json['city'] ?? '',
state: json['state'] ?? '',
country: json['country'] ?? '',
// type: List<String>.from(json['type'] ?? []),
thumbnail: json['thumbnail'] ?? '',
translations: (json['translations'] as List<dynamic>?)
?.map((item) => TranslationModel.fromJson(item as Map<String, dynamic>))
.toList() ?? [],
);
}
}

216
lib/models/item_model.dart Normal file
View File

@ -0,0 +1,216 @@
class TranslationModel {
final int id;
final String locale;
TranslationModel({required this.id, required this.locale});
Map<String, dynamic> toJson() {
return {'id': id, 'language': locale};
}
factory TranslationModel.fromJson(Map<String, dynamic> json) {
return TranslationModel(id: json['id'] ?? 0, locale: json['locale'] ?? '');
}
}
class Files {
final String? youtube;
final String? video;
final String? audio;
final String? booklet;
final String? simple;
Files({this.youtube, this.video, this.audio, this.booklet, this.simple});
Map<String, dynamic> toJson() {
return {
'youtube': youtube,
'video': video,
'audio': audio,
'booklet': booklet,
'simple': simple,
};
}
factory Files.fromJson(Map<String, dynamic> json) {
return Files(
youtube: json['youtube'] as String?,
video: json['video'] as String?,
audio: json['audio'] as String?,
booklet: json['booklet'] as String?,
simple: json['simple'] as String?,
);
}
}
class DurationField {
final int duration;
DurationField({required this.duration});
factory DurationField.fromJson(Map<String, dynamic> json) {
// convert duration from string to int
final dynamic rawDuration = json['duration'];
final int parsedDuration = rawDuration is String
? int.tryParse(rawDuration) ?? 0
: rawDuration is int
? rawDuration
: 0;
return DurationField(duration: parsedDuration);
}
}
class ItemModel {
final String id;
final String title;
final DateTime date;
final String type;
final String? slug;
bool rm;
String biblestudy;
bool draft;
int timestamp;
String activity;
DurationField duration;
String place;
String city;
String state;
String country;
// List<String> type;
String thumbnail;
List<TranslationModel> translations;
Files files;
ItemModel({
required this.id,
required this.title,
required this.date,
this.type = '',
this.slug,
this.rm = false,
this.biblestudy = '',
this.draft = false,
this.timestamp = 0,
this.activity = '',
required this.duration,
this.place = '',
this.city = '',
this.state = '',
this.country = '',
// this.type = const [],
this.thumbnail = '',
this.translations = const [],
Files? files,
}) : files = files ?? Files();
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'date': date.toIso8601String(),
'type': type,
'slug': slug,
'rm': rm,
'biblestudy': biblestudy,
'draft': draft,
'timestamp': timestamp,
'activity': activity,
'duration': duration,
'place': place,
'city': city,
'state': state,
'country': country,
'thumbnail': thumbnail.toString(),
'translations': translations.toString(),
'files': files.toString(),
};
}
factory ItemModel.fromJson(Map<String, dynamic> json) {
return ItemModel(
id: json['id'] ?? '',
title: json['title'] ?? '',
date:
json['date'] != null
? DateTime.parse(json['date'].toString())
: DateTime.now(),
type: json['type'] ?? '',
slug: json['slug'],
rm: json['rm'] ?? false,
biblestudy: json['biblestudy'] ?? '',
draft: json['draft'] ?? false,
timestamp: json['timestamp'] ?? 0,
activity: json['activity'] ?? '',
duration: DurationField.fromJson({'duration': json['duration']}),
place: json['place'] ?? '',
city: json['city'] ?? '',
state: json['state'] ?? '',
country: json['country'] ?? '',
// type: List<String>.from(json['type'] ?? []),
thumbnail: json['thumbnail'] ?? '',
translations:
(json['translations'] as List<dynamic>?)
?.map(
(item) =>
TranslationModel.fromJson(item as Map<String, dynamic>),
)
.toList() ??
[],
files:
json['files'] != null
? (json['files'] is Map<String, dynamic>
? Files.fromJson(json['files'] as Map<String, dynamic>)
: Files())
: Files(),
);
}
}
class ResultModel {
final List<YearModel> years;
ResultModel({required this.years});
factory ResultModel.fromJson(Map<String, dynamic> json) {
return ResultModel(
years: (json['years'] as List)
.map((e) => YearModel.fromJson(e))
.toList(),
);
}
}
class YearModel {
final int year;
final int count;
final List<MonthModel> months;
YearModel({required this.year, required this.count, required this.months});
factory YearModel.fromJson(Map<String, dynamic> json) {
return YearModel(
year: json['year'],
count: json['count'],
months: (json['months'] as List)
.map((e) => MonthModel.fromJson(e))
.toList(),
);
}
}
class MonthModel {
final String name;
final String month;
final int count;
MonthModel({required this.name, required this.month, required this.count});
factory MonthModel.fromJson(Map<String, dynamic> json) {
return MonthModel(
name: json['name'],
month: json['month'],
count: int.parse(json['count']),
);
}
}

51
lib/navigation_bar.dart Normal file
View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import 'package:lgcc/pages/home.dart';
import 'package:lgcc/pages/items_list.dart';
class MainNavigationBar extends StatefulWidget {
@override _MainNavigationBarState createState() => _MainNavigationBarState();
}
class _MainNavigationBarState extends State<MainNavigationBar> {
int _currentIndex =0;
final List<Widget> _pages = [
HomePage(),
ItemsList(type: 'conferencias'),
ItemsList(type: 'actividades'),
];
void _onTabTapped(int index) {
setState(() {
_currentIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: _onTabTapped,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'Inicio',
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: 'Conferencias',
),
BottomNavigationBarItem(
icon: Icon(Icons.list),
label: 'Publicaciones',
),
],
),
);
}
}

View File

@ -1,88 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:lgcc/models/conferences_model.dart';
import 'package:lgcc/providers/directus_service.dart';
import 'package:lgcc/pages/detail_conference.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class ConferencesPage extends StatelessWidget {
final dynamic confereceDetail;
const ConferencesPage({super.key, this.confereceDetail});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Conferencias'),
centerTitle: true,
backgroundColor: Color(0xFFE5EBFD),
),
body: _buildConferenceListView(),
);
}
}
Widget _buildConferenceListView() {
final DirectusService _directusService = DirectusService();
return FutureBuilder(
future: Future.wait([_directusService.getConferences()]),
builder: (BuildContext context, AsyncSnapshot<List<dynamic>> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
print('Error: ${snapshot.error}');
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No conferences found.'));
} else {
List<ConferencesModel> conferences = List<ConferencesModel>.from(
snapshot.data![0],
);
return ListView.builder(
itemCount: conferences.length,
itemBuilder: (context, index) {
final conference = conferences[index];
String urlImage;
if (conference.thumbnail.isEmpty) {
urlImage =
'https://ik.imagekit.io/lgccc/tr:w-1920,f-auto/youtube_thumbnail_46396.png';
} else {
urlImage =
'https://directus.carpa.com/assets/${conference.thumbnail}?access_token=${dotenv.env['DIRECTUS_API_TOKEN']}';
}
return InkWell(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailConference(conference: conference),
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Image.network(
urlImage,
fit: BoxFit.cover,
width: double.infinity,
height: 200,
),
const SizedBox(height: 8),
Text(conference.title, style: const TextStyle(fontSize: 18)),
],
),
),
),
),
);
},
);
}
},
);
}

View File

@ -1,31 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:lgcc/models/conferences_model.dart';
import 'package:lgcc/models/item_model.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:intl/intl.dart';
class DetailConference extends StatelessWidget {
final ConferencesModel conference;
const DetailConference({super.key, required this.conference});
class DetailItem extends StatefulWidget {
final ItemModel items;
const DetailItem({super.key, required this.items});
@override
State<DetailItem> createState() => _DetailItemState();
}
class _DetailItemState extends State<DetailItem> {
@override
Widget build(BuildContext context) {
// Check if the conference has a thumbnail, if not use a default image
String urlImage;
if (conference.thumbnail.isEmpty) {
urlImage =
'https://ik.imagekit.io/lgccc/tr:w-1920,f-auto/youtube_thumbnail_46396.png';
} else {
urlImage =
'https://directus.carpa.com/assets/${conference.thumbnail}?access_token=${dotenv.env['DIRECTUS_API_TOKEN']}';
}
// String urlImage;
// if (widget.items.thumbnail.isEmpty) {
// urlImage =
// 'https://ik.imagekit.io/lgccc/tr:w-1920,f-auto/youtube_thumbnail_46396.png';
// } else {
// urlImage =
// 'https://actividadeswp.carpa.com/v5/uploads/?path=${widget.items.thumbnail}';
// }
final formattedDate = DateFormat.yMMMMEEEEd().format(conference.date);
final formattedDate = DateFormat.yMMMMEEEEd().format(widget.items.date);
return Scaffold(
appBar: AppBar(
title: const Text('Conferencias'),
title: Text(widget.items.title),
centerTitle: true,
backgroundColor: Color(0xFFE5EBFD),
),
@ -38,10 +43,10 @@ class DetailConference extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Image.network(urlImage),
// Image.network(urlImage),
const SizedBox(height: 8),
Text(
conference.title,
widget.items.title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -53,11 +58,11 @@ class DetailConference extends StatelessWidget {
style: const TextStyle(fontSize: 16, color: Colors.black),
),
Text(
'${conference.place}, ${conference.city}, ${conference.state}, ${conference.country}',
'${widget.items.place}, ${widget.items.city}, ${widget.items.state}, ${widget.items.country}',
style: const TextStyle(fontSize: 16, color: Colors.black),
),
Text(
'Actividad: ${conference.activity}',
'Actividad: ${widget.items.activity}',
style: const TextStyle(fontSize: 16, color: Colors.black),
),
],

15
lib/pages/home.dart Normal file
View File

@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold();
}
}

62
lib/pages/items_list.dart Normal file
View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:lgcc/models/item_model.dart';
import 'package:lgcc/providers/wordpress_service.dart';
import 'package:lgcc/pages/detail_item.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class ItemsList extends StatelessWidget {
final dynamic itemDetail;
final String type;
const ItemsList({Key? key, required this.type, this.itemDetail})
: super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(type),
centerTitle: true,
backgroundColor: Color(0xFFE5EBFD),
),
body: _buildItemListView(context),
);
}
Widget _buildItemListView(BuildContext context) {
final WordpressService wordpressService = WordpressService();
return FutureBuilder<List<dynamic>>(
future: wordpressService.fetchItems(type),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No hay items disponibles.'));
} else {
final List<ItemModel> items = snapshot.data!.cast<ItemModel>();
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
title: Text(item.title),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailItem(items: item),
),
);
},
);
},
);
}
},
);
}
}

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:lgcc/models/conferences_model.dart';
import 'package:lgcc/models/item_model.dart';
class DirectusService {
@ -17,7 +17,7 @@ Future<List<dynamic>> getConferences() async {
}());
if (response.statusCode == 200) {
final data = jsonDecode(response.body)['data'];
return data.map((item) => ConferencesModel.fromJson(item)).toList();
return data.map((item) => ItemModel.fromJson(item)).toList();
} else {
throw Exception('Failed to load conferences');
}

View File

@ -0,0 +1,35 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:lgcc/models/item_model.dart';
class WordpressService {
String getBaseUrlType(String type) {
return 'https://${type}wp.carpa.com/v5/items/?cache=false';
}
Future<List<ItemModel>> fetchItems(String type) async {
final baseUrl = getBaseUrlType(type);
final response = await http.get(Uri.parse('$baseUrl&f=list'));
print('Fetching items from: $response');
if (response.statusCode == 200) {
final List<dynamic> jsonData = json.decode(response.body);
return jsonData.map((item) => ItemModel.fromJson(item)).toList();
} else {
throw Exception('Failed to load items');
}
}
Future<List<ResultModel>> fetchYearsParam(String type ) async {
final baseUrl = getBaseUrlType(type);
final response = await http.get(Uri.parse(baseUrl + '&years'));
print('Fetching years from: $baseUrl');
if (response.statusCode == 200) {
final List<dynamic> jsonData = json.decode(response.body);
return jsonData.map((item) => ResultModel.fromJson(item)).toList();
} else {
throw Exception('Failed to load years');
}
}
}

View File

@ -131,6 +131,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
google_nav_bar:
dependency: "direct main"
description:
name: google_nav_bar
sha256: bb12dd21514ee1b041ab3127673e2fd85e693337df308f7f2b75cd1e8e92eaf4
url: "https://pub.dev"
source: hosted
version: "5.0.7"
html:
dependency: transitive
description:

View File

@ -13,6 +13,7 @@ dependencies:
flutter_dotenv: ^5.2.1
flutter_html: ^3.0.0
flutter_markdown: ^0.7.7+1
google_nav_bar: ^5.0.7
http: ^1.4.0
intl: ^0.20.2
path: ^1.9.1