CRUD working
This commit is contained in:
+25
-1
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 +0,0 @@
|
||||
// gets all quotes from the database and returns them in a list
|
||||
@@ -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.";
|
||||
}
|
||||
Reference in New Issue
Block a user