Compare commits

...

20 Commits

Author SHA1 Message Date
b66da23887 depending on the path a different language is pre-selected 2024-12-18 19:41:00 +01:00
6587828a0b content localized on locale change 2024-12-18 19:30:13 +01:00
a984fc15b0 added localizations for content 2024-12-18 18:41:37 +01:00
0d9118ab3e fixed typo 2024-12-18 18:22:14 +01:00
85b5d4dc7c updated gitignore and changed content directory 2024-12-18 18:21:37 +01:00
ebf1246a55 added month localizations 2024-12-18 18:17:22 +01:00
8e91388ecf Localized static text 2024-12-18 18:05:14 +01:00
b9033014c8 added more localized terms 2024-12-18 18:01:41 +01:00
0a877525aa added localization 2024-12-18 17:56:33 +01:00
ff97898b90 simple language dropdown without function 2024-12-18 17:36:24 +01:00
c523b7495f Fixed content not being responsive 2024-12-18 17:16:39 +01:00
4c2162a158 added additional skills 2024-12-05 20:45:00 +01:00
ee705938f7 Changed Font and Theme 2024-12-05 20:26:34 +01:00
35e1171623 Changed colors 2024-12-05 20:06:18 +01:00
6ebfa77416 Added "About me"-Section 2024-12-05 16:19:45 +01:00
b8840ffe0c Changed Title of LandingPage 2024-12-05 15:57:46 +01:00
c62dbbb707 Fixed bug that caused the skills list to display 100% every time 2024-12-05 15:56:46 +01:00
7fac0160e0 Refactoring 2024-12-05 15:54:29 +01:00
3ba6f9d714 Language-Widget added 2024-12-05 15:54:23 +01:00
683f0174ef Disallowed indexing 2024-12-03 01:15:57 +01:00
28 changed files with 766 additions and 245 deletions

2
.gitignore vendored
View File

@@ -156,4 +156,4 @@ app.*.symbols
!.vscode/settings.json !.vscode/settings.json
# Custom # Custom
content.json content/

BIN
assets/de_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

Binary file not shown.

Binary file not shown.

BIN
assets/gb_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

3
devtools_options.yaml Normal file
View File

@@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

3
l10n.yaml Normal file
View File

@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

8
lib/constants.dart Normal file
View File

@@ -0,0 +1,8 @@
enum ContentType {
experience,
education,
skills,
language,
text,
generalSkills
}

28
lib/l10n/app_de.arb Normal file
View File

@@ -0,0 +1,28 @@
{
"german": "Deutsch",
"english": "Englisch",
"resume": "Lebenslauf",
"skills": "Fähigkeiten",
"languages": "Sprachen",
"mother_tongue": "Muttersprache",
"very_good": "Sehr gut",
"about_me": "Über mich",
"additional_skills": "Weitere Kenntnisse",
"work_experience": "Arbeitserfahrung",
"education": "Bildungsweg",
"source_code": "Quellcode",
"@_months": {},
"january": "Januar",
"february": "Februar",
"march": "März",
"april": "April",
"may": "Mai",
"june": "Juni",
"july": "Juli",
"august": "August",
"september": "September",
"october": "Oktober",
"november": "November",
"december": "Dezember"
}

28
lib/l10n/app_en.arb Normal file
View File

@@ -0,0 +1,28 @@
{
"german": "German",
"english": "English",
"resume": "Resume",
"skills": "Skills",
"languages": "Languages",
"mother_tongue": "Native",
"very_good": "Very good",
"about_me": "About me",
"additional_skills": "Additional Skills",
"work_experience": "Work experience",
"education": "Education",
"source_code": "Source Code",
"@_months": {},
"january": "January",
"february": "February",
"march": "March",
"april": "April",
"may": "May",
"june": "June",
"july": "July",
"august": "August",
"september": "September",
"october": "October",
"november": "November",
"december": "December"
}

View File

@@ -1,26 +1,60 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:resume/pages/landing_page.dart'; import 'package:resume/pages/landing_page.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:resume/providers/locale_provider.dart';
import 'package:resume/providers/content_provider.dart';
import './services/tools.dart';
import 'theme.dart' show darkTheme;
void main() { void main() {
runApp(const Resume()); final String initialPath = Uri.base.path;
final String defaultLocale = Tools.getLocaleFromPath(initialPath);
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(
create: (context) =>
LocaleProvider(defaultLocale: Locale(defaultLocale)),
),
ChangeNotifierProvider(create: (context) => ContentProvider()),
],
child: const Resume(),
),
);
} }
class Resume extends StatelessWidget { class Resume extends StatefulWidget {
const Resume({super.key}); const Resume({super.key});
@override
State<Resume> createState() => _ResumeState();
}
class _ResumeState extends State<Resume> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return Consumer<LocaleProvider>(
title: 'Resume', builder: (context, localeProvider, child) => MaterialApp(
theme: ThemeData( title: 'Resume',
colorScheme: ColorScheme.fromSeed( theme: darkTheme,
seedColor: Colors.deepPurple, brightness: Brightness.dark), localizationsDelegates: const [
useMaterial3: true, AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('en'),
Locale('de'),
],
locale: localeProvider.locale,
routes: {
'/': (context) => const LandingPage(),
},
initialRoute: '/',
), ),
routes: {
'/': (context) => const LandingPage(),
},
initialRoute: '/',
); );
} }
} }

View File

@@ -1,9 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:resume/providers/locale_provider.dart';
import 'package:resume/services/breakpoints.dart'; import 'package:resume/services/breakpoints.dart';
import 'package:resume/services/content_provider.dart'; import 'package:resume/widgets/language_dropdown.dart';
import 'package:resume/widgets/content_widget.dart';
import 'package:resume/widgets/profile.dart'; import 'package:resume/widgets/profile.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:resume/constants.dart' show ContentType;
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../providers/content_provider.dart';
import '../widgets/content_block.dart';
class LandingPage extends StatefulWidget { class LandingPage extends StatefulWidget {
const LandingPage({super.key}); const LandingPage({super.key});
@@ -16,14 +22,18 @@ class LandingPage extends StatefulWidget {
class _LandingPageState extends State<LandingPage> { class _LandingPageState extends State<LandingPage> {
bool loadingDone = false; bool loadingDone = false;
late ContentProvider contentProvider;
late Locale currentLocale;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
await ContentProvider.init(); await Provider.of<ContentProvider>(context, listen: false)
.loadContent(context);
setState(() => loadingDone = true); setState(() => loadingDone = true);
}); });
currentLocale = context.read<LocaleProvider>().locale;
} }
double _getMainContentWidth() { double _getMainContentWidth() {
@@ -40,26 +50,48 @@ class _LandingPageState extends State<LandingPage> {
(MediaQuery.of(context).size.width - _getMainContentWidth()) / 2; (MediaQuery.of(context).size.width - _getMainContentWidth()) / 2;
Widget _getSideBar() { Widget _getSideBar() {
return ContentBox( return Column(
title: 'Fähigkeiten', children: [
content: ContentProvider.skills, ContentBlock(
contentType: ContentType.skills, blockTitle: AppLocalizations.of(context)!.skills,
contentType: ContentType.skills,
content: contentProvider.getContent(ContentType.skills),
),
const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock(
blockTitle: AppLocalizations.of(context)!.languages,
contentType: ContentType.language,
content: contentProvider.getContent(ContentType.language),
),
],
); );
} }
Widget _getMainContent() { Widget _getMainContent() {
return Column( return Column(
children: [ children: [
ContentBox( ContentBlock(
title: 'Arbeitserfahrung', blockTitle: AppLocalizations.of(context)!.work_experience,
content: ContentProvider.experience,
contentType: ContentType.experience, contentType: ContentType.experience,
content: contentProvider.getContent(ContentType.experience),
), ),
const Padding(padding: EdgeInsets.only(bottom: 25)), const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBox( ContentBlock(
title: 'Bildungsweg', blockTitle: AppLocalizations.of(context)!.education,
content: ContentProvider.education,
contentType: ContentType.education, contentType: ContentType.education,
content: contentProvider.getContent(ContentType.education),
),
const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock(
blockTitle: AppLocalizations.of(context)!.additional_skills,
contentType: ContentType.generalSkills,
content: contentProvider.getContent(ContentType.generalSkills),
),
const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock(
blockTitle: AppLocalizations.of(context)!.about_me,
contentType: ContentType.text,
content: contentProvider.getContent(ContentType.text),
), ),
], ],
); );
@@ -74,7 +106,7 @@ class _LandingPageState extends State<LandingPage> {
child: SizedBox( child: SizedBox(
width: _getSidebarWidth(), width: _getSidebarWidth(),
child: const Padding( child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 50), padding: EdgeInsets.symmetric(horizontal: 25),
child: Profile(), child: Profile(),
), ),
), ),
@@ -84,7 +116,7 @@ class _LandingPageState extends State<LandingPage> {
child: SizedBox( child: SizedBox(
width: _getSidebarWidth(), width: _getSidebarWidth(),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 50), padding: const EdgeInsets.symmetric(horizontal: 25),
child: _getSideBar(), child: _getSideBar(),
), ),
), ),
@@ -107,11 +139,12 @@ class _LandingPageState extends State<LandingPage> {
child: Column( child: Column(
children: [ children: [
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 50), padding: EdgeInsets.symmetric(horizontal: 25),
child: Profile(), child: Profile(),
), ),
const Padding(padding: EdgeInsets.only(bottom: 25)),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 50), padding: const EdgeInsets.symmetric(horizontal: 25),
child: _getSideBar(), child: _getSideBar(),
), ),
], ],
@@ -121,7 +154,7 @@ class _LandingPageState extends State<LandingPage> {
width: 900, width: 900,
child: _getMainContent(), child: _getMainContent(),
), ),
const Padding(padding: EdgeInsets.only(right: 50)), const Padding(padding: EdgeInsets.only(right: 25)),
], ],
); );
} else if (constraints.maxWidth > Breakpoints.lg) { } else if (constraints.maxWidth > Breakpoints.lg) {
@@ -136,6 +169,7 @@ class _LandingPageState extends State<LandingPage> {
child: Column( child: Column(
children: [ children: [
const Profile(), const Profile(),
const Padding(padding: EdgeInsets.only(bottom: 25)),
_getSideBar(), _getSideBar(),
], ],
), ),
@@ -166,11 +200,28 @@ class _LandingPageState extends State<LandingPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
contentProvider = context.read<ContentProvider>();
if (currentLocale != context.read<LocaleProvider>().locale) {
loadingDone = false;
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Provider.of<ContentProvider>(context, listen: false)
.loadContent(context);
setState(() {
currentLocale = context.read<LocaleProvider>().locale;
loadingDone = true;
});
});
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Landing'), title: Text(AppLocalizations.of(context)!.resume),
actions: const [ actions: [
TextButton(onPressed: _launchURL, child: Text('Source Code')), TextButton(
onPressed: _launchURL,
child: Text(AppLocalizations.of(context)!.source_code)),
const LanguageDropdown(),
], ],
), ),
body: !loadingDone body: !loadingDone

View File

@@ -0,0 +1,58 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:resume/constants.dart';
import 'package:resume/providers/locale_provider.dart';
class ContentProvider extends ChangeNotifier {
static final ContentProvider _instance = ContentProvider._();
factory ContentProvider() => _instance;
ContentProvider._();
static const String _baseJsonPath = 'assets/content/content';
Map<String, dynamic> _content = {
'experience': <List<dynamic>>[],
'education': <List<dynamic>>[],
'skills': <List<dynamic>>[],
'text': <String>[],
'general_skills': <String>[],
};
Future<bool> loadContent(BuildContext context) async {
String currentLocale = context.read<LocaleProvider>().locale.languageCode;
final String localizedPath = '${_baseJsonPath}_$currentLocale.json';
try {
String file = await rootBundle.loadString(localizedPath);
_content = json.decode(file);
notifyListeners();
return true;
} catch (e) {
// If localized version fails, fall back to default German
String file = await rootBundle.loadString('${_baseJsonPath}_de.json');
_content = json.decode(file);
notifyListeners();
return true;
}
}
T getContent<T>(ContentType contentType) {
switch (contentType) {
case ContentType.experience:
return _content['experience'] as T;
case ContentType.education:
return _content['education'] as T;
case ContentType.skills:
return _content['skills'] as T;
case ContentType.text:
return _content['text'] as T;
case ContentType.generalSkills:
return _content['general_skills'] as T;
default:
return [] as T;
}
}
}

View File

@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
class LocaleProvider extends ChangeNotifier {
LocaleProvider({Locale? defaultLocale})
: _locale = defaultLocale ?? const Locale('de');
Locale _locale = const Locale('de');
Locale get locale => _locale;
void setLocale(Locale locale) {
_locale = locale;
notifyListeners();
}
}

View File

@@ -1,27 +0,0 @@
import 'dart:convert';
import 'package:flutter/services.dart';
class ContentProvider {
ContentProvider._();
static const String _jsonPath = 'assets/content.json';
static Future<bool> init() async {
try {
String file = await rootBundle.loadString(_jsonPath);
_content = json.decode(file);
} catch (e) {
return false;
}
return true;
}
static Map<String, dynamic> _content = {};
static List<dynamic> get experience => _content['experience'];
static List<dynamic> get education => _content['education'];
static List<dynamic> get skills => _content['skills'];
}

108
lib/services/tools.dart Normal file
View File

@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class Tools {
Tools._();
/// Builds a formatted time string from two date strings in the format 'YYYY-MM'.
///
/// Returns a string in the format 'Month Year - Month Year' using localized month names.
/// Returns an empty string if either input is empty or invalid.
///
/// Parameters:
/// - [startDate]: String in 'YYYY-MM' format (e.g., '2024-03')
/// - [endDate]: String in 'YYYY-MM' format (e.g., '2024-12')
/// - [context]: BuildContext for accessing localizations
///
/// Example:
/// ```dart
/// final timeString = buildTimeString('2024-03', '2024-12', context);
/// // Returns "March 2024 - December 2024" (or localized equivalent)
/// ```
static String buildTimeString(
String startDate, String endDate, BuildContext context) {
// Check for empty or null inputs
if (startDate.isEmpty || endDate.isEmpty) {
return '';
}
try {
// Validate date format
if (!startDate.contains(RegExp(r'^\d{4}-\d{2}$')) ||
!endDate.contains(RegExp(r'^\d{4}-\d{2}$'))) {
return '';
}
// Parse dates with validation
final firstDate = DateTime.tryParse('$startDate-01');
final secondDate = DateTime.tryParse('$endDate-01');
// Check if parsing was successful
if (firstDate == null || secondDate == null) {
return '';
}
// Build the formatted string
return '${getLocalizedMonth(context, firstDate.month)} ${firstDate.year} - '
'${getLocalizedMonth(context, secondDate.month)} ${secondDate.year}';
} catch (e) {
// Handle any unexpected errors
debugPrint('Error building time string: $e');
return '';
}
}
/// Returns the localized month name as a [String] based on the provided month number.
///
/// The [context] is used to access the app's localizations.
/// The [monthNumber] must be between 1 and 12, where 1 represents January
/// and 12 represents December.
///
/// Throws an [ArgumentError] if the month number is not between 1 and 12.
///
/// Example:
/// ```dart
/// final monthName = getLocalizedMonth(context, 3); // Returns "March" or "März"
/// ```
static String getLocalizedMonth(BuildContext context, int monthNumber) {
final localizations = AppLocalizations.of(context)!;
switch (monthNumber) {
case 1:
return localizations.january;
case 2:
return localizations.february;
case 3:
return localizations.march;
case 4:
return localizations.april;
case 5:
return localizations.may;
case 6:
return localizations.june;
case 7:
return localizations.july;
case 8:
return localizations.august;
case 9:
return localizations.september;
case 10:
return localizations.october;
case 11:
return localizations.november;
case 12:
return localizations.december;
default:
throw ArgumentError('Month number must be between 1 and 12');
}
}
static String getLocaleFromPath(String path) {
if (path.startsWith('/de')) {
return 'de';
} else if (path.startsWith('/en')) {
return 'en';
}
return 'de'; // Default fallback
}
}

10
lib/theme.dart Normal file
View File

@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';
ThemeData get darkTheme => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xAAbb71fc),
brightness: Brightness.dark,
),
useMaterial3: true,
fontFamily: 'SourceSerif4',
);

View File

@@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:resume/widgets/content_list_tile.dart';
import 'package:resume/widgets/language_widget.dart';
import 'package:resume/constants.dart' show ContentType;
class ContentBlock extends StatelessWidget {
const ContentBlock({
super.key,
required this.blockTitle,
required this.contentType,
required this.content,
});
final String blockTitle;
final ContentType contentType;
final dynamic content;
Widget get _getContentWidget {
if (contentType == ContentType.language) {
return const LanguageWidget();
} else if (contentType == ContentType.text ||
contentType == ContentType.generalSkills) {
return SizedBox(
width: double.infinity,
child: Text(content),
);
}
// List-based content-blocks
List<Widget> widgets = [];
for (var item in content) {
widgets.add(_buildListTile(item));
}
return Column(
children: widgets,
);
}
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
blockTitle,
style: Theme.of(context).textTheme.headlineMedium,
),
const Padding(padding: EdgeInsets.only(bottom: 8)),
_getContentWidget,
],
),
),
);
}
Widget _buildListTile(Map data) {
if (contentType == ContentType.experience) {
return ContentListTile.experience(
name: data['name'],
location: data['location'],
title: data['title'],
description: data['description'],
startDate: data['startDate'],
endDate: data['endDate'],
);
} else if (contentType == ContentType.education) {
return ContentListTile.education(
name: data['name'],
location: data['location'],
title: data['title'],
startDate: data['startDate'],
endDate: data['endDate'],
);
} else {
return ContentListTile.skills(
name: data['name'],
percentage: data['percentage'],
);
}
}
}

View File

@@ -1,90 +1,184 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:resume/services/breakpoints.dart'; import 'package:resume/services/breakpoints.dart';
import 'package:resume/constants.dart' show ContentType;
import '../services/tools.dart';
class ContentListTile extends StatelessWidget { class ContentListTile extends StatelessWidget {
const ContentListTile( const ContentListTile.education(
{super.key, {super.key,
this.name, required this.name,
this.location, required this.location,
this.title, required this.title,
this.description, required this.startDate,
this.startDate, required this.endDate})
this.endDate}); : description = '',
percentage = '',
_contentType = ContentType.education;
final String? name; const ContentListTile.experience(
final String? location; {super.key,
final String? title; required this.name,
final String? description; required this.location,
final String? startDate; required this.title,
final String? endDate; required this.description,
required this.startDate,
required this.endDate})
: percentage = '',
_contentType = ContentType.experience;
const ContentListTile.skills(
{super.key, required this.name, required this.percentage})
: description = '',
title = ',',
location = '',
startDate = '',
endDate = '',
_contentType = ContentType.skills;
final String description;
final String endDate;
final String location;
final String name;
final String percentage;
final String startDate;
final String title;
final ContentType _contentType;
Widget get _skillsListTile => ListTile(
contentPadding: const EdgeInsets.all(0),
title: Row(
children: [
Expanded(flex: 2, child: Text(name)),
const Padding(padding: EdgeInsets.only(bottom: 8)),
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: LinearProgressIndicator(
value: (double.tryParse(percentage) ?? 1) / 100,
),
),
),
],
),
);
Widget _getEducationListTile(BuildContext context, double screenWidth) =>
ListTile(
contentPadding: const EdgeInsets.all(0),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(name),
Text(', $location'),
if (Breakpoints.xl < screenWidth) const Text(' - '),
if (Breakpoints.xl < screenWidth)
Text(
title,
style:
TextStyle(color: Theme.of(context).colorScheme.primary),
),
],
),
if (Breakpoints.xl >= screenWidth)
Text(
title,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
],
),
titleAlignment: ListTileTitleAlignment.titleHeight,
subtitle: Breakpoints.sm >= screenWidth
? Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
Tools.buildTimeString(startDate, endDate, context),
style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
)
: null,
trailing: Breakpoints.sm < screenWidth
? Text(
Tools.buildTimeString(startDate, endDate, context),
style:
TextStyle(color: Theme.of(context).colorScheme.secondary),
)
: null,
);
Widget _getExperienceListTile(BuildContext context, double screenWidth) =>
ListTile(
contentPadding: const EdgeInsets.all(0),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Text(name),
Text(', $location'),
if (Breakpoints.xl < screenWidth) const Text(' - '),
if (Breakpoints.xl < screenWidth)
Text(
title,
style:
TextStyle(color: Theme.of(context).colorScheme.primary),
),
],
),
if (Breakpoints.xl >= screenWidth)
Text(
title,
style: TextStyle(color: Theme.of(context).colorScheme.primary),
),
],
),
titleAlignment: ListTileTitleAlignment.titleHeight,
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(padding: EdgeInsets.only(top: 8)),
if (Breakpoints.sm >= screenWidth)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
Tools.buildTimeString(startDate, endDate, context),
style: Theme.of(context).textTheme.labelSmall!.copyWith(
color: Theme.of(context).colorScheme.secondary,
),
),
),
Text(description),
],
),
trailing: Breakpoints.sm < screenWidth
? Text(
Tools.buildTimeString(startDate, endDate, context),
style:
TextStyle(color: Theme.of(context).colorScheme.secondary),
)
: null,
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width; final width = MediaQuery.of(context).size.width;
return ListTile( if (_contentType == ContentType.skills) {
contentPadding: const EdgeInsets.all(0), return _skillsListTile;
title: Column( } else if (_contentType == ContentType.education) {
crossAxisAlignment: CrossAxisAlignment.start, return _getEducationListTile(context, width);
children: [ } else if (_contentType == ContentType.experience) {
Row( return _getExperienceListTile(context, width);
mainAxisAlignment: MainAxisAlignment.start, } else {
children: [ return const Placeholder();
if (name != null) }
Text(
name!,
),
if (location != null) Text(', $location'),
if (title != null && Breakpoints.xl < width) Text(' - $title'),
],
),
if (title != null && Breakpoints.xl >= width) Text('$title'),
],
),
titleAlignment: ListTileTitleAlignment.titleHeight,
subtitle: description != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(padding: EdgeInsets.only(top: 8)),
if (startDate != null &&
endDate != null &&
Breakpoints.sm >= width)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
_getTimeString(startDate!, endDate!),
style: Theme.of(context).textTheme.labelSmall,
),
),
Text(description!),
],
)
: null,
trailing: startDate != null && endDate != null && Breakpoints.sm < width
? Text(_getTimeString(startDate!, endDate!))
: null,
);
} }
String _getTimeString(String startDate, String endDate) {
final firstDate = DateTime.parse('$startDate-01');
final secondDate = DateTime.parse('$endDate-01');
return '${months[firstDate.month - 1]} ${firstDate.year} - ${months[secondDate.month - 1]} ${secondDate.year}';
}
static const months = [
'Januar',
'Februar',
'März',
'April',
'Mai',
'Juni',
'July',
'August',
'September',
'Oktober',
'November',
'Dezember'
];
} }

View File

@@ -1,70 +0,0 @@
import 'package:flutter/material.dart';
import 'package:resume/widgets/content_list_tile.dart';
import 'package:resume/widgets/skill_list_tile.dart';
class ContentBox extends StatelessWidget {
const ContentBox({
super.key,
required this.title,
required this.content,
required this.contentType,
});
final ContentType contentType;
final List<dynamic> content;
final String title;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.headlineMedium,
),
const Padding(padding: EdgeInsets.only(bottom: 8)),
ListView.builder(
shrinkWrap: true,
itemCount: content.length,
itemBuilder: (context, index) => _buildListTile(content[index]),
),
],
),
),
);
}
Widget _buildListTile(Map data) {
switch (contentType) {
case ContentType.experience:
return ContentListTile(
name: data['name'],
location: data['location'],
title: data['title'],
description: data['description'],
startDate: data['startDate'],
endDate: data['endDate'],
);
case ContentType.education:
return ContentListTile(
name: data['name'],
location: data['location'],
title: data['title'],
startDate: data['startDate'],
endDate: data['endDate'],
);
case ContentType.skills:
return SkillListTile(
name: data['name'],
percentage: data['percentage'],
);
}
}
}
enum ContentType { experience, education, skills }

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import '../providers/locale_provider.dart';
class LanguageDropdown extends StatefulWidget {
const LanguageDropdown({super.key});
@override
State<LanguageDropdown> createState() => _LanguageDropdownState();
}
class _LanguageDropdownState extends State<LanguageDropdown> {
@override
Widget build(BuildContext context) {
LocaleProvider localeProvider = Provider.of<LocaleProvider>(context);
return DropdownButton<String>(
value: localeProvider.locale.languageCode,
items: [
DropdownMenuItem(
value: 'de',
child: getMenuItem(
AppLocalizations.of(context)!.german,
'assets/de_icon.png',
),
),
DropdownMenuItem(
value: 'en',
child: getMenuItem(
AppLocalizations.of(context)!.english,
'assets/gb_icon.png',
),
),
],
onChanged: (value) {
if (value == null) return;
localeProvider.setLocale(Locale(value));
},
);
}
Widget getMenuItem(String label, String imagePath) {
return Row(
children: [
Text(label),
const Padding(padding: EdgeInsets.only(right: 8)),
Image.asset(imagePath, width: 30),
],
);
}
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LanguageWidget extends StatelessWidget {
const LanguageWidget({super.key});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.asset(
'assets/de_icon.png',
width: 50,
),
const Padding(padding: EdgeInsets.only(right: 15)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context)!.german,
style: Theme.of(context).textTheme.bodyLarge),
Text(AppLocalizations.of(context)!.mother_tongue,
style: Theme.of(context).textTheme.bodyMedium),
],
),
],
),
const Padding(padding: EdgeInsets.only(bottom: 8)),
Row(
children: [
Image.asset(
'assets/gb_icon.png',
width: 50,
),
const Padding(padding: EdgeInsets.only(right: 15)),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(AppLocalizations.of(context)!.english,
style: Theme.of(context).textTheme.bodyLarge),
Text(AppLocalizations.of(context)!.very_good,
style: Theme.of(context).textTheme.bodyMedium),
],
),
],
),
],
);
}
}

View File

@@ -1,35 +0,0 @@
import 'package:flutter/material.dart';
class SkillListTile extends StatelessWidget {
const SkillListTile({
super.key,
required this.name,
this.percentage,
});
final String name;
final String? percentage;
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: const EdgeInsets.all(0),
title: Row(
children: [
Expanded(flex: 2, child: Text(name)),
const Padding(padding: EdgeInsets.only(bottom: 8)),
if (percentage != null)
Expanded(
flex: 5,
child: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: LinearProgressIndicator(
value: double.parse(percentage!) / 100,
),
),
),
],
),
);
}
}

View File

@@ -12,6 +12,10 @@ dependencies:
cupertino_icons: ^1.0.8 cupertino_icons: ^1.0.8
url_launcher: ^6.3.1 url_launcher: ^6.3.1
flutter_localizations:
sdk: flutter
intl: any
provider: ^6.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -20,8 +24,27 @@ dev_dependencies:
flutter_lints: ^4.0.0 flutter_lints: ^4.0.0
flutter: flutter:
generate: true
uses-material-design: true uses-material-design: true
assets: assets:
- assets/content.json - assets/content/content_de.json
- assets/content/content_en.json
- assets/profile.jpg - assets/profile.jpg
- assets/de_icon.png
- assets/gb_icon.png
fonts:
- family: Radley
fonts:
- asset: assets/fonts/radley/Radley-Regular.ttf
style: normal
- asset: assets/fonts/radley/Radley-Italic.ttf
style: italic
- family: SourceSerif4
fonts:
- asset: assets/fonts/sourceserif4/SourceSerif4-VariableFont_opsz,wght.ttf
style: normal
- asset: assets/fonts/sourceserif4/SourceSerif4-Italic-VariableFont_opsz,wght.ttf
style: italic

View File

@@ -1,6 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="robots" content="noindex">
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.

2
web/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-agent: *
Disallow: /