From 6ca462e2d20b7c8c237fa1ca69599640ab0fa7bc Mon Sep 17 00:00:00 2001 From: lukas-heiligenbrunner Date: Sat, 30 Dec 2023 16:46:13 +0100 Subject: [PATCH] add github action --- .github/workflows/docker-build.yml | 25 +++ backend/src/api/backend.rs | 6 +- backend/src/api/embed.rs | 4 + backend/src/api/list.rs | 31 +++- backend/src/main.rs | 4 +- frontend/lib/api/api_client.dart | 2 +- frontend/lib/api/builds.dart | 14 ++ frontend/lib/api/packages.dart | 3 - .../lib/components/app_button_widget.dart | 63 ------- .../components/dashboard/recent_builds.dart | 7 +- frontend/lib/components/input_widget.dart | 94 ---------- .../menu_shell.dart} | 11 +- frontend/lib/components/router.dart | 37 ++++ frontend/lib/components/side_menu.dart | 5 +- frontend/lib/components/wrapper.dart | 34 ---- frontend/lib/constants/color_constants.dart | 2 +- frontend/lib/main.dart | 12 +- frontend/lib/screens/build_screen.dart | 163 ++++++++++++++++++ frontend/pubspec.lock | 21 +++ frontend/pubspec.yaml | 1 + frontend/web/index.html | 2 +- 21 files changed, 325 insertions(+), 216 deletions(-) create mode 100644 .github/workflows/docker-build.yml delete mode 100644 frontend/lib/components/app_button_widget.dart delete mode 100644 frontend/lib/components/input_widget.dart rename frontend/lib/{screens/home_screen.dart => components/menu_shell.dart} (77%) create mode 100644 frontend/lib/components/router.dart delete mode 100644 frontend/lib/components/wrapper.dart create mode 100644 frontend/lib/screens/build_screen.dart diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..77b84e3 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,25 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - master + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: luki42 + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build and push Docker image + run: | + docker build -t luki42/aurcache:latest . + docker push luki42/aurcache:latest diff --git a/backend/src/api/backend.rs b/backend/src/api/backend.rs index b45185b..be997b6 100644 --- a/backend/src/api/backend.rs +++ b/backend/src/api/backend.rs @@ -1,6 +1,6 @@ use crate::api::add::okapi_add_operation_for_package_add_; use crate::api::add::package_add; -use crate::api::list::okapi_add_operation_for_list_builds_; +use crate::api::list::{get_build, okapi_add_operation_for_list_builds_}; use crate::api::list::okapi_add_operation_for_stats_; use crate::api::list::{build_output, okapi_add_operation_for_package_list_}; use crate::api::list::{list_builds, okapi_add_operation_for_search_}; @@ -8,6 +8,7 @@ use crate::api::list::{okapi_add_operation_for_build_output_, stats}; use crate::api::list::{package_list, search}; use crate::api::remove::okapi_add_operation_for_package_del_; use crate::api::remove::okapi_add_operation_for_version_del_; +use crate::api::list::okapi_add_operation_for_get_build_; use crate::api::remove::{package_del, version_del}; use rocket::Route; use rocket_okapi::openapi_get_routes; @@ -21,6 +22,7 @@ pub fn build_api() -> Vec { version_del, build_output, list_builds, - stats + stats, + get_build ] } diff --git a/backend/src/api/embed.rs b/backend/src/api/embed.rs index d6005b4..d4d1e99 100644 --- a/backend/src/api/embed.rs +++ b/backend/src/api/embed.rs @@ -32,6 +32,10 @@ impl Handler for CustomHandler { path = path.join("index.html") } + // if let None = path.extension() { + // path = "index.html".into(); + // } + match ::get(path.to_string_lossy().as_ref()) { None => Outcome::Failure(Status::NotFound), Some(file_content) => { diff --git a/backend/src/api/list.rs b/backend/src/api/list.rs index 7b1cc27..884fa7b 100644 --- a/backend/src/api/list.rs +++ b/backend/src/api/list.rs @@ -9,7 +9,7 @@ use rocket::serde::{Deserialize, Serialize}; use rocket::{get, State}; use rocket_okapi::okapi::schemars; use rocket_okapi::{openapi, JsonSchema}; -use sea_orm::PaginatorTrait; +use sea_orm::{PaginatorTrait}; use sea_orm::{ColumnTrait, QueryFilter}; use sea_orm::{DatabaseConnection, EntityTrait, FromQueryResult, QuerySelect, RelationTrait}; @@ -123,10 +123,11 @@ pub struct ListBuildsModel { } #[openapi(tag = "test")] -#[get("/builds?")] +#[get("/builds?&")] pub async fn list_builds( db: &State, pkgid: Option, + limit: Option, ) -> Result>, NotFound> { let db = db as &DatabaseConnection; @@ -137,7 +138,8 @@ pub async fn list_builds( .column_as(builds::Column::Id, "id") .column(builds::Column::Status) .column_as(packages::Column::Name, "pkg_name") - .column(versions::Column::Version); + .column(versions::Column::Version) + .limit(limit); let build = match pkgid { None => basequery.into_model::().all(db), @@ -152,6 +154,29 @@ pub async fn list_builds( Ok(Json(build)) } +#[openapi(tag = "test")] +#[get("/builds/")] +pub async fn get_build( + db: &State, + buildid: i32, +) -> Result, NotFound> { + let db = db as &DatabaseConnection; + + let result = Builds::find() + .join_rev(JoinType::InnerJoin, packages::Relation::Builds.def()) + .join_rev(JoinType::InnerJoin, versions::Relation::Builds.def()) + .filter(builds::Column::Id.eq(buildid)) + .select_only() + .column_as(builds::Column::Id, "id") + .column(builds::Column::Status) + .column_as(packages::Column::Name, "pkg_name") + .column(versions::Column::Version) + .into_model::() + .one(db).await.map_err(|e| NotFound(e.to_string()))?.ok_or(NotFound("no item with id found".to_string()))?; + + Ok(Json(result)) +} + #[derive(FromQueryResult, Deserialize, JsonSchema, Serialize)] #[serde(crate = "rocket::serde")] pub struct ListStats { diff --git a/backend/src/main.rs b/backend/src/main.rs index ca61096..2a08873 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -56,11 +56,11 @@ fn main() { let rock = rocket::custom(config) .manage(db) .manage(tx) - .mount("/", backend::build_api()) + .mount("/api/", backend::build_api()) .mount( "/docs/", make_swagger_ui(&SwaggerUIConfig { - url: "../openapi.json".to_owned(), + url: "../api/openapi.json".to_owned(), ..Default::default() }), ); diff --git a/frontend/lib/api/api_client.dart b/frontend/lib/api/api_client.dart index 6799fd2..eb4d2fd 100644 --- a/frontend/lib/api/api_client.dart +++ b/frontend/lib/api/api_client.dart @@ -2,7 +2,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; class ApiClient { - static const String _apiBase = kDebugMode ? "http://localhost:8081" : ""; + static const String _apiBase = !kDebugMode ? "http://localhost:8081/api" : "api"; final Dio _dio = Dio(BaseOptions(baseUrl: _apiBase)); String? token; diff --git a/frontend/lib/api/builds.dart b/frontend/lib/api/builds.dart index c7451e4..84ebeab 100644 --- a/frontend/lib/api/builds.dart +++ b/frontend/lib/api/builds.dart @@ -10,4 +10,18 @@ extension BuildsAPI on ApiClient { responseObject.map((e) => Build.fromJson(e)).toList(growable: false); return packages; } + + Future getBuild(int id) async { + final resp = await getRawClient().get("/builds/${id}"); + return Build.fromJson(resp.data); + } + + Future getOutput({int? line, required int buildID}) async { + String uri = "/builds/output?buildid=$buildID"; + if (line != null) { + uri += "&startline=$line"; + } + final resp = await getRawClient().get(uri); + return resp.data.toString(); + } } diff --git a/frontend/lib/api/packages.dart b/frontend/lib/api/packages.dart index f04fdbd..60ac6df 100644 --- a/frontend/lib/api/packages.dart +++ b/frontend/lib/api/packages.dart @@ -4,9 +4,6 @@ import 'api_client.dart'; extension PackagesAPI on ApiClient { Future> listPackages() async { final resp = await getRawClient().get("/packages/list"); - print(resp.data); - - // todo error handling final responseObject = resp.data as List; final List packages = diff --git a/frontend/lib/components/app_button_widget.dart b/frontend/lib/components/app_button_widget.dart deleted file mode 100644 index 3308a3f..0000000 --- a/frontend/lib/components/app_button_widget.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:flutter/material.dart'; - -enum ButtonType { PRIMARY, PLAIN } - -class AppButton extends StatelessWidget { - final ButtonType? type; - final VoidCallback? onPressed; - final String? text; - - AppButton({this.type, this.onPressed, this.text}); - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: this.onPressed, - child: Container( - width: double.infinity, - height: 45, - decoration: BoxDecoration( - color: getButtonColor(context, type!), - borderRadius: BorderRadius.circular(4.0), - boxShadow: [ - BoxShadow( - //color: Color.fromRGBO(169, 176, 185, 0.42), - //spreadRadius: 0, - //blurRadius: 3.0, - //offset: Offset(0, 2), - ) - ], - ), - child: Center( - child: Text(this.text!, - style: Theme.of(context) - .textTheme - .subtitle1! - .copyWith(color: getTextColor(context, type!))), - ), - ), - ); - } -} - -Color getButtonColor(context, ButtonType type) { - switch (type) { - case ButtonType.PRIMARY: - return Theme.of(context).buttonTheme.colorScheme!.background; - case ButtonType.PLAIN: - return Colors.white; - default: - return Theme.of(context).primaryColor; - } -} - -Color getTextColor(context, ButtonType type) { - switch (type) { - case ButtonType.PLAIN: - return Theme.of(context).primaryColor; - case ButtonType.PRIMARY: - return Colors.white; - default: - return Theme.of(context).buttonTheme.colorScheme!.background; - } -} diff --git a/frontend/lib/components/dashboard/recent_builds.dart b/frontend/lib/components/dashboard/recent_builds.dart index dd23dab..3bcf662 100644 --- a/frontend/lib/components/dashboard/recent_builds.dart +++ b/frontend/lib/components/dashboard/recent_builds.dart @@ -4,6 +4,7 @@ import 'package:aurcache/api/builds.dart'; import 'package:aurcache/models/build.dart'; import 'package:aurcache/components/dashboard/your_packages.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import '../../api/API.dart'; import '../../constants/color_constants.dart'; @@ -82,7 +83,7 @@ class _RecentBuildsState extends State { .toList(), ); } else { - return Text("no data"); + return const Text("no data"); } }), ), @@ -102,7 +103,9 @@ class _RecentBuildsState extends State { switchSuccessIcon(build.status), color: switchSuccessColor(build.status), ), - onPressed: () {}, + onPressed: () { + context.push("/build/${build.id}"); + }, )), ], ); diff --git a/frontend/lib/components/input_widget.dart b/frontend/lib/components/input_widget.dart deleted file mode 100644 index c40fb3f..0000000 --- a/frontend/lib/components/input_widget.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../constants/color_constants.dart'; - -class InputWidget extends StatelessWidget { - final String? hintText; - final String? errorText; - final Widget? prefixIcon; - final double? height; - final String? topLabel; - final bool? obscureText; - final FormFieldSetter? onSaved; - final ValueChanged? onChanged; - final FormFieldValidator? validator; - final TextInputType? keyboardType; - final Key? kKey; - final TextEditingController? kController; - final String? kInitialValue; - - InputWidget({ - this.hintText, - this.prefixIcon, - this.height = 48.0, - this.topLabel = "", - this.obscureText = false, - required this.onSaved, - this.keyboardType, - this.errorText, - this.onChanged, - this.validator, - this.kKey, - this.kController, - this.kInitialValue, - }); - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(this.topLabel!), - SizedBox(height: 4.0), - Container( - height: 50, - decoration: BoxDecoration( - color: secondaryColor, - //color: Theme.of(context).buttonColor, - borderRadius: BorderRadius.circular(4.0), - ), - child: TextFormField( - initialValue: this.kInitialValue, - controller: this.kController, - key: this.kKey, - keyboardType: this.keyboardType, - onSaved: this.onSaved, - onChanged: this.onChanged, - validator: this.validator, - obscureText: this.obscureText!, - decoration: InputDecoration( - prefixIcon: this.prefixIcon, - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Color.fromRGBO(74, 77, 84, 0.2), - ), - ), - focusedBorder: OutlineInputBorder( - //gapPadding: 16, - borderSide: BorderSide( - color: Theme.of(context).primaryColor, - ), - ), - errorStyle: TextStyle(height: 0, color: Colors.transparent), - errorBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).errorColor, - ), - ), - focusedErrorBorder: OutlineInputBorder( - //gapPaddings: 16, - borderSide: BorderSide( - color: Theme.of(context).errorColor, - ), - ), - hintText: this.hintText, - hintStyle: Theme.of(context) - .textTheme - .bodyText1! - .copyWith(color: Colors.white54), - errorText: this.errorText), - ), - ) - ], - ); - } -} diff --git a/frontend/lib/screens/home_screen.dart b/frontend/lib/components/menu_shell.dart similarity index 77% rename from frontend/lib/screens/home_screen.dart rename to frontend/lib/components/menu_shell.dart index b0db80e..d589d0f 100644 --- a/frontend/lib/screens/home_screen.dart +++ b/frontend/lib/components/menu_shell.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import '../utils/responsive.dart'; -import 'dashboard_screen.dart'; -import '../components/side_menu.dart'; +import '../screens/dashboard_screen.dart'; +import 'side_menu.dart'; -class HomeScreen extends StatelessWidget { - const HomeScreen({super.key}); +class MenuShell extends StatelessWidget { + const MenuShell({super.key, required this.child}); + final Widget child; @override Widget build(BuildContext context) { @@ -25,7 +26,7 @@ class HomeScreen extends StatelessWidget { Expanded( // It takes 5/6 part of the screen flex: 5, - child: DashboardScreen(), + child: child, ), ], ), diff --git a/frontend/lib/components/router.dart b/frontend/lib/components/router.dart new file mode 100644 index 0000000..6a980a2 --- /dev/null +++ b/frontend/lib/components/router.dart @@ -0,0 +1,37 @@ +import 'package:aurcache/screens/build_screen.dart'; +import 'package:aurcache/screens/dashboard_screen.dart'; +import 'package:aurcache/components/menu_shell.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +final GlobalKey _rootNavigatorKey = GlobalKey(); +final GlobalKey _shellNavigatorKey = + GlobalKey(); + +final appRouter = GoRouter( + navigatorKey: _rootNavigatorKey, + initialLocation: '/', + routes: [ + ShellRoute( + navigatorKey: _shellNavigatorKey, + builder: (context, state, child) { + return MenuShell(child: child); + }, + routes: [ + GoRoute( + path: '/', + builder: (context, state) => DashboardScreen(), + routes: [ + GoRoute( + path: 'build/:id', + builder: (context, state) { + final id = int.parse(state.pathParameters['id']!); + return BuildScreen(buildID: id); + }, + ), + ] + ), + ], + ), + ], +); diff --git a/frontend/lib/components/side_menu.dart b/frontend/lib/components/side_menu.dart index 911df9c..3316175 100644 --- a/frontend/lib/components/side_menu.dart +++ b/frontend/lib/components/side_menu.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; import '../constants/color_constants.dart'; @@ -35,7 +36,9 @@ class SideMenu extends StatelessWidget { DrawerListTile( title: "Dashboard", svgSrc: "assets/icons/menu_dashbord.svg", - press: () {}, + press: () { + context.go("/"); + }, ), DrawerListTile( title: "Builds", diff --git a/frontend/lib/components/wrapper.dart b/frontend/lib/components/wrapper.dart deleted file mode 100644 index 666c4d4..0000000 --- a/frontend/lib/components/wrapper.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/material.dart'; - -import '../constants/color_constants.dart'; - -class Wrapper extends StatelessWidget { - final Widget? title; - final Widget child; - - const Wrapper({Key? key, this.title, required this.child}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(defaultPadding), - decoration: BoxDecoration( - color: Palette.wrapperBg, - borderRadius: BorderRadius.circular(defaultBorderRadius), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (title != null) - Column( - children: [ - title!, - const SizedBox(height: defaultPadding), - ], - ), - child - ], - ), - ); - } -} diff --git a/frontend/lib/constants/color_constants.dart b/frontend/lib/constants/color_constants.dart index 28b7ce7..e361dd0 100644 --- a/frontend/lib/constants/color_constants.dart +++ b/frontend/lib/constants/color_constants.dart @@ -13,7 +13,7 @@ const defaultPadding = 16.0; const double defaultBorderRadius = 15; class ColorConstants { - static Color blue = Color(0xFF0D46BB); + static Color blue = const Color(0xFF0D46BB); } class Palette { diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 23b0b74..cf7e64b 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,10 +1,11 @@ -import 'package:aurcache/screens/home_screen.dart'; +import 'package:aurcache/components/router.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; - import 'constants/color_constants.dart'; void main() { + GoRouter.optionURLReflectsImperativeAPIs = true; runApp(const MyApp()); } @@ -13,7 +14,8 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return MaterialApp.router( + routerConfig: appRouter, debugShowCheckedModeBanner: false, title: 'Smart Dashboard - Admin Panel v0.1 ', theme: ThemeData.dark().copyWith( @@ -25,7 +27,9 @@ class MyApp extends StatelessWidget { .apply(bodyColor: Colors.white), canvasColor: secondaryColor, ), - home: const HomeScreen(), + routeInformationParser: appRouter.routeInformationParser, + routeInformationProvider: appRouter.routeInformationProvider, + routerDelegate: appRouter.routerDelegate, ); } } diff --git a/frontend/lib/screens/build_screen.dart b/frontend/lib/screens/build_screen.dart new file mode 100644 index 0000000..b802a66 --- /dev/null +++ b/frontend/lib/screens/build_screen.dart @@ -0,0 +1,163 @@ +import 'dart:async'; + +import 'package:aurcache/api/builds.dart'; +import 'package:aurcache/models/build.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../api/API.dart'; +import '../components/dashboard/your_packages.dart'; + +class BuildScreen extends StatefulWidget { + const BuildScreen({super.key, required this.buildID}); + + final int buildID; + + @override + State createState() => _BuildScreenState(); +} + +class _BuildScreenState extends State { + late Future buildData; + late Future initialOutput; + + String output = ""; + Timer? outputTimer, buildDataTimer; + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder( + future: buildData, + builder: (context, snapshot) { + if (snapshot.hasData) { + final buildData = snapshot.data!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + const SizedBox( + width: 10, + ), + IconButton( + icon: Icon( + switchSuccessIcon(buildData.status), + color: switchSuccessColor(buildData.status), + ), + onPressed: () { + context.replace("/build/${buildData.id}"); + }, + ), + const SizedBox( + width: 10, + ), + Text( + buildData.pkg_name, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox( + width: 10, + ), + const Text("triggered 2 months ago") + ], + ), + const SizedBox( + height: 15, + ), + Expanded( + flex: 1, + child: SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.vertical, //.horizontal + child: Padding( + padding: const EdgeInsets.only(left: 30, right: 15), + child: Text( + output, + style: const TextStyle( + fontSize: 16.0, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ); + } else { + return const Text("loading build"); + } + }), + appBar: AppBar(), + ); + } + + @override + void initState() { + super.initState(); + + initBuildDataLoader(); + initOutputLoader(); + } + + void initBuildDataLoader() { + buildData = API.getBuild(widget.buildID); + buildDataTimer = Timer.periodic(const Duration(seconds: 10), (t) { + setState(() { + buildData = API.getBuild(widget.buildID); + }); + }); + } + + void initOutputLoader() { + initialOutput = API.getOutput(buildID: widget.buildID); + initialOutput.then((value) { + setState(() { + output = value; + }); + _scrollToBottom(); + }); + + buildData.then((value) { + // poll new output only if not finished + if (value.status == 0) { + outputTimer = + Timer.periodic(const Duration(seconds: 3), (Timer t) async { + print("refreshing output"); + final value = await API.getOutput( + buildID: widget.buildID, line: output.split("\n").length); + setState(() { + output += value; + }); + + _scrollToBottom(); + }); + } + }); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + // scroll to bottom + final scrollPosition = scrollController.position; + if (scrollPosition.viewportDimension < scrollPosition.maxScrollExtent) { + scrollController.animateTo( + scrollPosition.maxScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + }); + } + + @override + void dispose() { + super.dispose(); + outputTimer?.cancel(); + buildDataTimer?.cancel(); + } +} diff --git a/frontend/pubspec.lock b/frontend/pubspec.lock index 1afddb5..a1b8bdb 100644 --- a/frontend/pubspec.lock +++ b/frontend/pubspec.lock @@ -131,6 +131,19 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: ca7e4a2249f96773152f1853fa25933ac752495cdd7fdf5dafb9691bd05830fd + url: "https://pub.dev" + source: hosted + version: "13.0.0" google_fonts: dependency: "direct main" description: @@ -163,6 +176,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" matcher: dependency: transitive description: diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml index 9d6819a..3783e59 100644 --- a/frontend/pubspec.yaml +++ b/frontend/pubspec.yaml @@ -39,6 +39,7 @@ dependencies: flutter_svg: ^2.0.9 google_fonts: ^6.1.0 dio: ^5.3.3 + go_router: ^13.0.0 dev_dependencies: flutter_test: diff --git a/frontend/web/index.html b/frontend/web/index.html index fa8c0e0..a449226 100644 --- a/frontend/web/index.html +++ b/frontend/web/index.html @@ -14,7 +14,7 @@ This is a placeholder for base href that will be replaced by the value of the `--base-href` argument provided to `flutter build`. --> - +