first commit

This commit is contained in:
2026-04-29 12:53:22 +07:00
commit e6a30eddd3
394 changed files with 16408 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
class AppValidators {
AppValidators._();
static String? required(
String value, {
String message = 'Field tidak boleh kosong',
}) {
if (value.trim().isEmpty) {
return message;
}
return null;
}
static String? minLength(
String value,
int min, {
String? message,
}) {
if (value.length < min) {
return message ?? 'Minimal $min karakter';
}
return null;
}
static String? maxLength(
String value,
int max, {
String? message,
}) {
if (value.length > max) {
return message ?? 'Maksimal $max karakter';
}
return null;
}
static final _emailRegex = RegExp(r'^[\w\.-]+@([\w-]+\.)+[\w-]{2,}$');
static String? email(
String value, {
String emptyMessage = 'Email tidak boleh kosong',
String invalidMessage = 'Format email tidak valid',
}) {
if (value.trim().isEmpty) {
return emptyMessage;
}
if (!_emailRegex.hasMatch(value.trim())) {
return invalidMessage;
}
return null;
}
static final _phoneRegex = RegExp(r'^[0-9]{9,15}$');
static String? phone(
String value, {
String emptyMessage = 'Nomor telepon tidak boleh kosong',
String invalidMessage = 'Nomor telepon tidak valid',
}) {
final v = value.replaceAll(RegExp('[^0-9]'), '');
if (v.isEmpty) {
return emptyMessage;
}
if (!_phoneRegex.hasMatch(v)) {
return invalidMessage;
}
return null;
}
static String? password(
String value, {
int minLength = 8,
bool requireUppercase = true,
bool requireNumber = true,
bool requireSymbol = false,
}) {
if (value.isEmpty) {
return 'Password tidak boleh kosong';
}
if (value.length < minLength) {
return 'Password minimal $minLength karakter';
}
if (requireUppercase && !value.contains(RegExp('[A-Z]'))) {
return 'Password harus mengandung huruf besar';
}
if (requireNumber && !value.contains(RegExp('[0-9]'))) {
return 'Password harus mengandung angka';
}
if (requireSymbol && !value.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
return 'Password harus mengandung simbol';
}
return null;
}
static String? number(
String value, {
String message = 'Harus berupa angka',
}) {
if (value.isEmpty) return null;
if (num.tryParse(value) == null) {
return message;
}
return null;
}
static String? minNumber(
String value,
num min, {
String? message,
}) {
final n = num.tryParse(value);
if (n == null) return 'Harus berupa angka';
if (n < min) {
return message ?? 'Minimal $min';
}
return null;
}
static String? maxNumber(
String value,
num max, {
String? message,
}) {
final n = num.tryParse(value);
if (n == null) return 'Harus berupa angka';
if (n > max) {
return message ?? 'Maksimal $max';
}
return null;
}
static String? equals(
String value,
String otherValue, {
String message = 'Nilai tidak sama',
}) {
if (value != otherValue) {
return message;
}
return null;
}
static String? compose(
String value,
List<String? Function(String)> validators,
) {
for (final validator in validators) {
final result = validator(value);
if (result != null) return result;
}
return null;
}
}

View File

@@ -0,0 +1,80 @@
import 'package:intl/intl.dart';
class DateTimeUtils {
DateTimeUtils._();
static const _locale = 'id_ID';
static String yyyyMMdd(DateTime date) {
return DateFormat('yyyy-MM-dd', _locale).format(date);
}
static String ddMMMyyyy(DateTime date) {
return DateFormat('dd MMM yyyy', _locale).format(date);
}
static String ddMMMMyyyy(DateTime date) {
return DateFormat('dd MMMM yyyy', _locale).format(date);
}
static String ddMMMyyyyHHmm(DateTime date) {
return DateFormat('dd MMM yyyy HH:mm', _locale).format(date);
}
static String hhMM(DateTime date) {
return DateFormat('HH:mm', _locale).format(date);
}
static String hhMMss(DateTime date) {
return DateFormat('HH:mm:ss', _locale).format(date);
}
static String dayDDMMMMyyyy(DateTime date) {
return DateFormat('EEEE, dd MMMM yyyy', _locale).format(date);
}
static String dayDDMMMMyyyyHHmm(DateTime date) {
return DateFormat('EEEE, dd MMMM yyyy HH:mm:ss', _locale).format(date);
}
static DateTime? parse(String? value) {
if (value == null || value.isEmpty) return null;
return DateTime.tryParse(value);
}
static String toApi(DateTime date) {
return DateFormat("yyyy-MM-dd'T'HH:mm:ss", _locale).format(date);
}
static String timeAgo(DateTime date) {
final diff = DateTime.now().difference(date);
if (diff.inSeconds < 60) {
return '${diff.inSeconds} detik lalu';
} else if (diff.inMinutes < 60) {
return '${diff.inMinutes} menit lalu';
} else if (diff.inHours < 24) {
return '${diff.inHours} jam lalu';
} else if (diff.inDays < 7) {
return '${diff.inDays} hari lalu';
}
return dayDDMMMMyyyy(date);
}
static String formatDateTimeWithGMT(DateTime date) {
final formatted = DateFormat(
'MMM dd, yyyy hh:mm:ss a',
_locale,
).format(date);
final offset = date.timeZoneOffset;
final sign = offset.isNegative ? '-' : '+';
final hours = offset.inHours.abs().toString().padLeft(2, '0');
final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0');
return '$formatted GMT$sign$hours:$minutes';
}
}

View File

@@ -0,0 +1,5 @@
String formatFileSize(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(1)} KB';
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}

View File

@@ -0,0 +1,58 @@
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
class SessionManager {
static const _refreshTokenKey = 'refreshToken';
static const _tokenKey = 'token';
static const _userKey = 'user';
static Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_tokenKey, token);
}
static Future<void> saveRefreshToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_refreshTokenKey, token);
}
static Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_tokenKey);
}
static Future<String?> getRefreshToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_refreshTokenKey);
}
static Future<void> saveUser(Map<String, dynamic> user) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_userKey, jsonEncode(user));
}
static Future<Map<String, dynamic>?> getUser() async {
final prefs = await SharedPreferences.getInstance();
final data = prefs.getString(_userKey);
if (data == null) return null;
return jsonDecode(data) as Map<String, dynamic>;
}
static Future<String?> getUserName() async {
final user = await getUser();
return user?['name']?.toString() ?? '';
}
static Future<String?> getNrp() async {
final user = await getUser();
return user?['nrp']?.toString() ?? '';
}
static Future<void> clearSession() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tokenKey);
await prefs.remove(_userKey);
}
}

View File

@@ -0,0 +1,181 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:frontend_eccp_mobile/app/core/constants/app_colors.dart';
import 'package:frontend_eccp_mobile/app/core/constants/enum/snackbar_type_enum.dart';
void showMessage(
BuildContext context,
String message,
SnackBarType snackBarType, {
int duration = 2000,
}) {
final overlay = Overlay.of(context);
final indicatorColor = switch (snackBarType) {
SnackBarType.error => AppColors.primary900,
SnackBarType.success => AppColors.statusSuccess,
SnackBarType.warning => AppColors.statusWarning,
_ => const Color(0xFF7FAAF6),
};
late OverlayEntry entry;
entry = OverlayEntry(
builder: (_) => _TopSnackBar(
message: message,
duration: duration,
indicatorColor: indicatorColor,
onDismiss: () {
entry.remove();
},
),
);
overlay.insert(entry);
}
class _TopSnackBar extends StatefulWidget {
const _TopSnackBar({
required this.message,
required this.duration,
required this.indicatorColor,
required this.onDismiss,
});
final String message;
final int duration;
final Color indicatorColor;
final VoidCallback onDismiss;
@override
State<_TopSnackBar> createState() => _TopSnackBarState();
}
class _TopSnackBarState extends State<_TopSnackBar>
with SingleTickerProviderStateMixin {
late AnimationController controller;
late Animation<Offset> slideAnimation;
late Animation<double> opacityAnimation;
@override
void initState() {
super.initState();
controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 620),
reverseDuration: const Duration(milliseconds: 520),
);
slideAnimation =
Tween<Offset>(
begin: const Offset(0, -2),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
),
);
opacityAnimation =
Tween<double>(
begin: 0,
end: 1,
).animate(
CurvedAnimation(
parent: controller,
curve: Curves.easeOut,
),
);
unawaited(controller.forward());
unawaited(_startTimer());
}
Future<void> _startTimer() async {
await Future<void>.delayed(Duration(milliseconds: widget.duration));
await dismiss();
}
Future<void> dismiss() async {
if (!mounted) return;
await controller.reverse();
widget.onDismiss();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Positioned(
top: MediaQuery.of(context).padding.top + 8,
left: 16.w,
right: 16.w,
child: SlideTransition(
position: slideAnimation,
child: FadeTransition(
opacity: opacityAnimation,
child: Material(
color: Colors.transparent,
child: GestureDetector(
onTap: dismiss,
child: Container(
constraints: BoxConstraints(maxWidth: 600.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8.r),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10.r,
offset: const Offset(0, 4),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.fromLTRB(16.w, 12.h, 16.w, 0),
child: Text(
widget.message,
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
),
SizedBox(height: 12.h),
TweenAnimationBuilder<double>(
duration: Duration(milliseconds: widget.duration),
tween: Tween(begin: 1, end: 0),
builder: (_, value, _) {
return LinearProgressIndicator(
value: value,
color: widget.indicatorColor,
backgroundColor: Colors.transparent,
minHeight: 4.h,
);
},
),
],
),
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,50 @@
import 'dart:developer';
import 'package:shared_preferences/shared_preferences.dart';
class TokenManager {
static const _tokenKey = 'token';
static const _refreshTokenKey = 'refreshToken';
static const _oneTimeFlagPrefix = 'one_time_flag__';
static Future<void> setOneTimeFlag(
String name, {
required bool value,
}) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('$_oneTimeFlagPrefix$name', value);
}
static Future<bool> getOneTimeFlag(String name) async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool('$_oneTimeFlagPrefix$name') ?? false;
}
static Future<void> clearOneTimeFlag(
String name,
) async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('$_oneTimeFlagPrefix$name');
}
static Future<void> saveRefreshToken(String token) async {
log('SAVE REFRESH TOKEN: $token');
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_refreshTokenKey, token);
}
static Future<void> clearToken() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_refreshTokenKey, '');
await prefs.setString(_tokenKey, '');
}
static Future<String?> getRefreshToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_refreshTokenKey);
}
static Future<void> removeToken() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_tokenKey);
}
}

View File

@@ -0,0 +1,20 @@
class VersionUtils {
static bool isNewer(String remote, String local) {
final r = _parse(remote);
final l = _parse(local);
for (var i = 0; i < 3; i++) {
if (r[i] > l[i]) return true;
if (r[i] < l[i]) return false;
}
return false;
}
static List<int> _parse(String v) {
final parts = v.split('.');
return List.generate(
3,
(i) => i < parts.length ? int.tryParse(parts[i]) ?? 0 : 0,
);
}
}