CRUD working

This commit is contained in:
Odweta
2026-03-22 20:05:31 +01:00
parent 0219155f8e
commit a95d899ac5
11 changed files with 462 additions and 11 deletions
+25 -1
View File
@@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'strings.dart';
import 'main_scaffold.dart';
// providers
import 'provider/quote_provider.dart';
// pages
import 'pages/list.dart';
import 'pages/add.dart';
@@ -12,7 +16,12 @@ import 'pages/leaderboard.dart';
import 'pages/settings.dart';
void main() {
runApp(HlaskovnikFrontendApp());
runApp(
ChangeNotifierProvider(
create: (_) => QuoteProvider(),
child: HlaskovnikFrontendApp(),
),
);
}
class HlaskovnikFrontendApp extends StatelessWidget {
@@ -39,6 +48,21 @@ class HlaskovnikFrontendApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp.router(
title: Strings.appName,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.red,
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.red,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: ThemeMode.system,
routerConfig: _router,
);
}
+10 -2
View File
@@ -1,13 +1,21 @@
// quote model class
class Quote{
final int index;
final int id;
final String author;
final String body;
const Quote({
required this.index,
required this.id,
required this.author,
required this.body,
});
factory Quote.fromJson(Map<String, dynamic> json) {
return Quote(
id: json['id'],
author: json['author'],
body: json['body'],
);
}
}
+78 -1
View File
@@ -1,10 +1,87 @@
import 'package:flutter/material.dart';
import 'package:frontend/provider/quote_provider.dart';
import 'package:frontend/model/quote.dart';
import 'package:frontend/strings.dart';
class AddPage extends StatelessWidget {
const AddPage({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: Text('add page!'));
final bodyController = TextEditingController(text: '');
final authorController = TextEditingController(text: '');
final provider = QuoteProvider();
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
Strings.createQuote,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
TextField(
autofocus: true,
controller: bodyController,
decoration: const InputDecoration(
labelText: Strings.quote,
),
),
const SizedBox(height: 8),
TextField(
controller: authorController,
decoration: const InputDecoration(
labelText: Strings.author,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
bodyController.clear();
authorController.clear();
},
child: const Text(Strings.cancel),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {
final quote = Quote(
id: 0,
body: bodyController.text,
author: authorController.text,
);
provider.createQuote(quote);
bodyController.clear();
authorController.clear();
},
child: const Text(Strings.save),
),
],
),
],
),
),
);
}
}
+144 -1
View File
@@ -1,10 +1,153 @@
import 'package:flutter/material.dart';
import 'package:frontend/provider/quote_provider.dart';
import 'package:frontend/model/quote.dart';
import 'package:frontend/strings.dart';
import 'package:provider/provider.dart';
class ListPage extends StatelessWidget {
const ListPage({super.key});
@override
Widget build(BuildContext context) {
return Center(child: const Text('list page!'));
final provider = context.watch<QuoteProvider>();
return RefreshIndicator(
onRefresh: () async {
await provider.readQuotes();
},
child: _bodyBuilder(provider),
);
}
Widget _bodyBuilder(QuoteProvider provider) {
if (provider.loading) {
return Center(child: CircularProgressIndicator());
}
if (provider.error != null) {
return Center(child: Text(provider.error!));
}
return
ListView.builder(
itemCount: provider.quotes.length,
itemBuilder: (context, index) {
final quote = provider.quotes[index];
return
ListTile(
title: Text(quote.body),
subtitle: Text(" ${quote.author}"),
trailing: PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
if (value == 'update') {
// open a dialog window that allows for mods
_showUpdateDialog(context, provider, quote);
} else if (value == 'delete') {
// open a confirmation dialog window for deletion
_showDeleteDialog(context, provider, quote);
}
},
itemBuilder: (context) => [
const PopupMenuItem(
value: 'update',
child: Row(
children: [
Icon(Icons.edit),
SizedBox(width: 10),
Text(Strings.updateQuote),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete),
SizedBox(width: 10),
Text(Strings.deleteQuote),
],
),
),
],
)
);
},
);
}
void _showUpdateDialog(BuildContext context, QuoteProvider provider, Quote quote) {
final bodyController = TextEditingController(text: quote.body);
final authorController = TextEditingController(text: quote.author);
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(Strings.updateQuote),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
autofocus: true,
controller: bodyController,
),
TextField(
controller: authorController,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(Strings.cancel),
),
ElevatedButton(
onPressed: () {
final updatedQuote = Quote(
id: quote.id,
body: bodyController.text,
author: authorController.text,
);
Navigator.pop(context);
provider.updateQuote(updatedQuote);
},
child: const Text(Strings.save),
),
],
);
},
);
}
void _showDeleteDialog(BuildContext context, QuoteProvider provider, Quote quote) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('${Strings.deleteQuote}?'),
content: const Text(Strings.noUndo),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text(Strings.cancel),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () {
Navigator.pop(context);
provider.deleteQuote(quote);
},
child: const Text(Strings.delete),
),
],
);
},
);
}
}
+69
View File
@@ -0,0 +1,69 @@
// gets all quotes from the database and returns them in a list
import 'package:flutter/material.dart';
import 'package:frontend/model/quote.dart';
import 'package:frontend/repository/quote_repository.dart';
class QuoteProvider extends ChangeNotifier {
List<Quote> _quotes = [];
bool _loading = false;
String? _error;
List<Quote> get quotes => _quotes;
bool get loading => _loading;
String? get error => _error;
final QuoteRepository repo = QuoteRepository();
QuoteProvider() {
readQuotes(); // auto-load on creation
}
Future<void> readQuotes() async {
_loading = true;
_error = null;
notifyListeners();
try {
_quotes = await repo.readQuotes();
} catch (e) {
_error = e.toString();
}
_loading = false;
notifyListeners();
}
Future<void> createQuote(Quote quote) async {
final newQuote = await repo.createQuote(quote);
_quotes.add(newQuote);
notifyListeners();
}
Future<void> updateQuote(Quote quote) async {
try {
_loading = true;
notifyListeners();
final updated = await repo.updateQuote(quote);
final index = _quotes.indexWhere((q) => q.id == quote.id);
if (index != -1) {
final newList = List<Quote>.from(_quotes);
newList[index] = updated;
_quotes = newList;
}
} finally {
_loading = false;
notifyListeners();
}
}
Future<void> deleteQuote(Quote quote) async {
await repo.deleteQuote(quote);
_quotes.removeWhere((q) => q.id == quote.id);
notifyListeners();
}
}
+72 -3
View File
@@ -1,7 +1,76 @@
// talks to the DB itself
// TODO: mock for now!
// TODO: implement the real one
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:frontend/model/quote.dart';
class QuoteRepository {
final String _dbBaseUrl = 'https://hlaskovnik-reimagined-latest.onrender.com';
static final String _apiBaseUrl = 'https://hlaskovnik-reimagined-latest.onrender.com';
static final String _apiKey = "2ogD5kc4sw3JVZoouQSV0w3LGRjSj5IjVFnAnk7IRCIK0ktONCeoOYinbNLSIHuU";
static final String _quotesEndpoint = '/quotes';
Future<List<Quote>> readQuotes() async {
final url = Uri.parse(_apiBaseUrl+_quotesEndpoint);
final headers = {
'Authorization' : 'Bearer $_apiKey',
'Content-Type': 'application/json',
};
final response = await http.get(url, headers: headers);
if (response.statusCode == 200) {
final List data = jsonDecode(response.body);
return data.map((json) => Quote.fromJson(json)).toList();
} else {
throw Exception('failed to read quotes from database');
}
}
Future<Quote> createQuote(Quote quote) async {
final url = Uri.parse(_apiBaseUrl+_quotesEndpoint);
final body = jsonEncode({'author' : quote.author, 'body' : quote.body});
final headers = {
'Authorization' : 'Bearer $_apiKey',
'Content-Type': 'application/json',
};
final response = await http.post(url, body: body, headers: headers);
if (response.statusCode == 200 || response.statusCode == 201) {
return Quote.fromJson(jsonDecode(response.body));
} else {
throw Exception('failed to add quote to database');
}
}
Future<Quote> updateQuote(Quote quote) async {
final url = Uri.parse('$_apiBaseUrl$_quotesEndpoint/${quote.id}');
final body = jsonEncode({'body' : quote.body, 'author' : quote.author});
final headers = {
'Authorization' : 'Bearer $_apiKey',
'Content-Type': 'application/json',
};
final response = await http.put(url, body: body, headers: headers);
if (response.statusCode == 200) {
return Quote.fromJson(jsonDecode(response.body));
}
if (response.statusCode == 204) {
return quote;
}
throw Exception('failed to update quote in database');
}
Future<void> deleteQuote(Quote quote) async {
final url = Uri.parse('$_apiBaseUrl$_quotesEndpoint/${quote.id}');
final headers = {
'Authorization' : 'Bearer $_apiKey',
'Content-Type': 'application/json',
};
final response = await http.delete(url, headers: headers);
if (response.statusCode == 200 || response.statusCode == 204) {
return;
}
throw Exception('failed to delete quote from database');
}
}
@@ -0,0 +1,16 @@
// talks to the DB itself
// TODO: mock for now!
import 'package:frontend/model/quote.dart';
class QuoteRepository {
//final String _dbBaseUrl = 'https://hlaskovnik-reimagined-latest.onrender.com';
Future<List<Quote>> getQuotes() async {
return [
Quote(id: 1, author: 'Adam', body: 'kratom je vule bozi'),
Quote(id: 2, author: 'Vojta', body: 'kalach s kecupem'),
Quote(id: 3, author: 'Anton', body: 'have you ever noticed how long cows are'),
];
}
}
-1
View File
@@ -1 +0,0 @@
// gets all quotes from the database and returns them in a list
+14 -1
View File
@@ -1,10 +1,23 @@
class Strings {
static const String appName = "Nový Hláškovník";
static const String apiKey = "2ogD5kc4sw3JVZoouQSV0w3LGRjSj5IjVFnAnk7IRCIK0ktONCeoOYinbNLSIHuU";
static const String list = "Seznam";
static const String add = "Přidat";
static const String flashcards = "Flashkarty";
static const String leaderboard = "Žebříčky";
static const String settings = "Nastavení";
static const String createQuote = "Přidat hlášku";
static const String updateQuote = "Upravit hlášku";
static const String deleteQuote = "Smazat hlášku";
static const String update = "Upravit";
static const String cancel = "Zrušit";
static const String delete = "Smazat";
static const String save = "Uložit";
static const String quote = "Hláška";
static const String author = "Autor";
static const String noUndo = "Tuto akci nelze odčinit.";
}