Compare commits

..

24 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
fc1823aec0 Refactoring 2024-12-03 01:06:58 +01:00
634ecce4d9 Implemented breaking points for varios screen sizes 2024-12-03 00:54:49 +01:00
69675f42e2 added url_launcher to link to code repository 2024-12-02 19:32:14 +01:00
da4a057fd7 Changed font size 2024-12-02 19:23:22 +01:00
29 changed files with 920 additions and 287 deletions

2
.gitignore vendored
View File

@@ -156,4 +156,4 @@ app.*.symbols
!.vscode/settings.json
# 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_localizations/flutter_localizations.dart';
import 'package:provider/provider.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() {
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});
@override
State<Resume> createState() => _ResumeState();
}
class _ResumeState extends State<Resume> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Resume',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.deepPurple, brightness: Brightness.dark),
useMaterial3: true,
return Consumer<LocaleProvider>(
builder: (context, localeProvider, child) => MaterialApp(
title: 'Resume',
theme: darkTheme,
localizationsDelegates: const [
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,8 +1,15 @@
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/content_provider.dart';
import 'package:resume/widgets/content_widget.dart';
import 'package:resume/widgets/language_dropdown.dart';
import 'package:resume/widgets/profile.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 {
const LandingPage({super.key});
@@ -15,17 +22,21 @@ class LandingPage extends StatefulWidget {
class _LandingPageState extends State<LandingPage> {
bool loadingDone = false;
late ContentProvider contentProvider;
late Locale currentLocale;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await ContentProvider.init();
await Provider.of<ContentProvider>(context, listen: false)
.loadContent(context);
setState(() => loadingDone = true);
});
currentLocale = context.read<LocaleProvider>().locale;
}
double _getPageWidth() {
double _getMainContentWidth() {
final width = MediaQuery.of(context).size.width;
// if (width < Breakpoints.sm) return width;
if (width < Breakpoints.md) return width * 0.95;
@@ -35,18 +46,185 @@ class _LandingPageState extends State<LandingPage> {
return width * 0.5;
}
double _getSidebarWidth() =>
(MediaQuery.of(context).size.width - _getMainContentWidth()) / 2;
Widget _getSideBar() {
return Column(
children: [
ContentBlock(
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() {
return Column(
children: [
ContentBlock(
blockTitle: AppLocalizations.of(context)!.work_experience,
contentType: ContentType.experience,
content: contentProvider.getContent(ContentType.experience),
),
const Padding(padding: EdgeInsets.only(bottom: 25)),
ContentBlock(
blockTitle: AppLocalizations.of(context)!.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),
),
],
);
}
Widget _buildLayout(BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth > Breakpoints.xl2) {
return Stack(
children: [
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: _getSidebarWidth(),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 25),
child: Profile(),
),
),
),
Align(
alignment: Alignment.topRight,
child: SizedBox(
width: _getSidebarWidth(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: _getSideBar(),
),
),
),
Center(
child: SizedBox(
width: _getMainContentWidth(),
child: _getMainContent(),
),
),
],
);
} else if (constraints.maxWidth > Breakpoints.xl) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Column(
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 25),
child: Profile(),
),
const Padding(padding: EdgeInsets.only(bottom: 25)),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: _getSideBar(),
),
],
),
),
SizedBox(
width: 900,
child: _getMainContent(),
),
const Padding(padding: EdgeInsets.only(right: 25)),
],
);
} else if (constraints.maxWidth > Breakpoints.lg) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Column(
children: [
const Profile(),
const Padding(padding: EdgeInsets.only(bottom: 25)),
_getSideBar(),
],
),
),
),
SizedBox(
width: 650,
child: _getMainContent(),
),
const Padding(padding: EdgeInsets.only(right: 25)),
],
);
} else {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 25),
child: Column(
children: [
const Profile(),
const Padding(padding: EdgeInsets.only(bottom: 25)),
_getMainContent(),
const Padding(padding: EdgeInsets.only(bottom: 25)),
_getSideBar(),
],
),
);
}
}
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
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(
appBar: AppBar(
title: const Text('Landing'),
title: Text(AppLocalizations.of(context)!.resume),
actions: [
TextButton(onPressed: () {}, child: const Text('Source Code')),
TextButton(
onPressed: _launchURL,
child: Text(AppLocalizations.of(context)!.source_code)),
const LanguageDropdown(),
],
),
body: !loadingDone
// While the content is being loaded from JSON, show a LoadingIndicator
? const Center(
child: Column(
mainAxisSize: MainAxisSize.max,
@@ -59,63 +237,15 @@ class _LandingPageState extends State<LandingPage> {
),
)
: SingleChildScrollView(
child: Stack(
children: [
Align(
alignment: Alignment.topLeft,
child: SizedBox(
width: (MediaQuery.of(context).size.width -
_getPageWidth()) /
2,
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 50),
child: Profile(),
),
),
),
Align(
alignment: Alignment.topRight,
child: SizedBox(
width: (MediaQuery.of(context).size.width -
_getPageWidth()) /
2,
child: screenWidth >= Breakpoints.xl2
? Padding(
padding:
const EdgeInsets.symmetric(horizontal: 50),
child: ContentBox(
title: 'Fähigkeiten',
content: ContentProvider.skills,
contentType: ContentType.skills,
),
)
: null,
),
),
Center(
child: SizedBox(
width: _getPageWidth(),
child: Column(
children: [
ContentBox(
title: 'Arbeitserfahrung',
content: ContentProvider.experience,
contentType: ContentType.experience,
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 15)),
ContentBox(
title: 'Bildungsweg',
content: ContentProvider.education,
contentType: ContentType.education,
),
],
),
),
),
],
child: LayoutBuilder(
builder: _buildLayout,
),
),
);
}
}
void _launchURL() async {
final Uri url = Uri.parse('https://git.skup.in/marco/resume');
await launchUrl(url);
}

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,69 +1,184 @@
import 'package:flutter/material.dart';
import 'package:resume/services/breakpoints.dart';
import 'package:resume/constants.dart' show ContentType;
import '../services/tools.dart';
class ContentListTile extends StatelessWidget {
const ContentListTile(
const ContentListTile.education(
{super.key,
this.name,
this.location,
this.title,
this.description,
this.startDate,
this.endDate});
required this.name,
required this.location,
required this.title,
required this.startDate,
required this.endDate})
: description = '',
percentage = '',
_contentType = ContentType.education;
final String? name;
final String? location;
final String? title;
final String? description;
final String? startDate;
final String? endDate;
const ContentListTile.experience(
{super.key,
required this.name,
required this.location,
required this.title,
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
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
return ListTile(
contentPadding: const EdgeInsets.all(0),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
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 ? Text(description!) : null,
trailing: startDate != null && endDate != null
? Text(_getTimeString(startDate!, endDate!))
: null,
);
if (_contentType == ContentType.skills) {
return _skillsListTile;
} else if (_contentType == ContentType.education) {
return _getEducationListTile(context, width);
} else if (_contentType == ContentType.experience) {
return _getExperienceListTile(context, width);
} else {
return const Placeholder();
}
}
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 = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December'
];
}

View File

@@ -1,69 +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,
),
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

@@ -6,33 +6,39 @@ class Profile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Container(
constraints: const BoxConstraints(maxWidth: 200),
child: ClipRRect(
borderRadius: BorderRadius.circular(180),
child: Image.asset('assets/profile.jpg'),
child: SizedBox(
width: double.infinity,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
children: [
Container(
constraints: const BoxConstraints(maxWidth: 200),
child: ClipRRect(
borderRadius: BorderRadius.circular(180),
child: Image.asset('assets/profile.jpg'),
),
),
),
const Padding(padding: EdgeInsets.symmetric(vertical: 5)),
Text(
'Marco Skupin',
style: Theme.of(context).textTheme.displayMedium,
),
const Padding(padding: EdgeInsets.symmetric(vertical: 5)),
Text(
'Master of Science',
style: Theme.of(context).textTheme.titleLarge,
),
const Padding(padding: EdgeInsets.symmetric(vertical: 5)),
Text(
'marco@skup.in',
style: Theme.of(context).textTheme.bodyLarge,
),
],
const Padding(padding: EdgeInsets.symmetric(vertical: 5)),
Text(
'Marco Skupin',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.displayMedium,
),
const Padding(padding: EdgeInsets.symmetric(vertical: 5)),
Text(
'Master of Science',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge,
),
const Padding(padding: EdgeInsets.symmetric(vertical: 5)),
Text(
'marco@skup.in',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
),
);

View File

@@ -1,31 +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)),
if (percentage != null)
Expanded(
flex: 5,
child: LinearProgressIndicator(
value: double.parse(percentage!) / 100,
),
),
],
),
);
}
}

View File

@@ -11,6 +11,11 @@ dependencies:
sdk: flutter
cupertino_icons: ^1.0.8
url_launcher: ^6.3.1
flutter_localizations:
sdk: flutter
intl: any
provider: ^6.1.2
dev_dependencies:
flutter_test:
@@ -19,8 +24,27 @@ dev_dependencies:
flutter_lints: ^4.0.0
flutter:
generate: true
uses-material-design: true
assets:
- assets/content.json
- assets/profile.jpg
- assets/content/content_de.json
- assets/content/content_en.json
- 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>
<html>
<head>
<meta name="robots" content="noindex">
<!--
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.

2
web/robots.txt Normal file
View File

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