Compare commits
3 Commits
e2288467b7
...
cc1af29e02
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc1af29e02 | ||
|
|
b4ac86068e | ||
|
|
14e9aee02f |
44
.gitignore
vendored
@@ -1,43 +1 @@
|
|||||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
node_modules
|
||||||
|
|
||||||
# dependencies
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Expo
|
|
||||||
.expo/
|
|
||||||
dist/
|
|
||||||
web-build/
|
|
||||||
expo-env.d.ts
|
|
||||||
|
|
||||||
# Native
|
|
||||||
.kotlin/
|
|
||||||
*.orig.*
|
|
||||||
*.jks
|
|
||||||
*.p8
|
|
||||||
*.p12
|
|
||||||
*.key
|
|
||||||
*.mobileprovision
|
|
||||||
|
|
||||||
# Metro
|
|
||||||
.metro-health-check*
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.*
|
|
||||||
yarn-debug.*
|
|
||||||
yarn-error.*
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
app-example
|
|
||||||
|
|
||||||
# generated native folders
|
|
||||||
/ios
|
|
||||||
/android
|
|
||||||
|
|||||||
43
apps/client/.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Expo
|
||||||
|
.expo/
|
||||||
|
dist/
|
||||||
|
web-build/
|
||||||
|
expo-env.d.ts
|
||||||
|
|
||||||
|
# Native
|
||||||
|
.kotlin/
|
||||||
|
*.orig.*
|
||||||
|
*.jks
|
||||||
|
*.p8
|
||||||
|
*.p12
|
||||||
|
*.key
|
||||||
|
*.mobileprovision
|
||||||
|
|
||||||
|
# Metro
|
||||||
|
.metro-health-check*
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.*
|
||||||
|
yarn-debug.*
|
||||||
|
yarn-error.*
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
app-example
|
||||||
|
|
||||||
|
# generated native folders
|
||||||
|
/ios
|
||||||
|
/android
|
||||||
10
apps/client/eslint.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// https://docs.expo.dev/guides/using-eslint/
|
||||||
|
const { defineConfig } = require('eslint/config');
|
||||||
|
const expoConfig = require('eslint-config-expo/flat');
|
||||||
|
|
||||||
|
module.exports = defineConfig([
|
||||||
|
expoConfig,
|
||||||
|
{
|
||||||
|
ignores: ['dist/*'],
|
||||||
|
},
|
||||||
|
]);
|
||||||
50
apps/client/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@caldav/client",
|
||||||
|
"main": "expo-router/entry",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"start": "expo start",
|
||||||
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
|
"android": "expo start --android",
|
||||||
|
"ios": "expo start --ios",
|
||||||
|
"web": "expo start --web",
|
||||||
|
"lint": "expo lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@caldav/shared": "*",
|
||||||
|
"@expo/vector-icons": "^15.0.3",
|
||||||
|
"@react-navigation/bottom-tabs": "^7.4.0",
|
||||||
|
"@react-navigation/elements": "^2.6.3",
|
||||||
|
"@react-navigation/native": "^7.1.8",
|
||||||
|
"@shopify/flash-list": "^2.0.2",
|
||||||
|
"expo": "~54.0.25",
|
||||||
|
"expo-constants": "~18.0.10",
|
||||||
|
"expo-font": "~14.0.9",
|
||||||
|
"expo-haptics": "~15.0.7",
|
||||||
|
"expo-image": "~3.0.10",
|
||||||
|
"expo-linking": "~8.0.9",
|
||||||
|
"expo-router": "~6.0.15",
|
||||||
|
"expo-splash-screen": "~31.0.11",
|
||||||
|
"expo-status-bar": "~3.0.8",
|
||||||
|
"expo-symbols": "~1.0.7",
|
||||||
|
"expo-system-ui": "~6.0.8",
|
||||||
|
"expo-web-browser": "~15.0.9",
|
||||||
|
"nativewind": "^4.2.1",
|
||||||
|
"react": "19.1.0",
|
||||||
|
"react-dom": "19.1.0",
|
||||||
|
"react-native": "0.81.5",
|
||||||
|
"react-native-gesture-handler": "~2.28.0",
|
||||||
|
"react-native-reanimated": "~4.1.1",
|
||||||
|
"react-native-safe-area-context": "5.6.0",
|
||||||
|
"react-native-screens": "~4.16.0",
|
||||||
|
"react-native-web": "~0.21.0",
|
||||||
|
"react-native-worklets": "0.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "~19.1.0",
|
||||||
|
"eslint-config-expo": "~10.0.0",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||||
|
"tailwindcss": "^3.4.17"
|
||||||
|
},
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
308
apps/client/src/app/Calender.tsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
import { Animated, Modal, Pressable, Text, View } from "react-native";
|
||||||
|
import { DAYS, MONTHS, Month } from "../Constants";
|
||||||
|
import Header from "../components/Header";
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import currentTheme from "../Themes";
|
||||||
|
import BaseBackground from "../components/BaseBackground";
|
||||||
|
import { FlashList } from "@shopify/flash-list";
|
||||||
|
|
||||||
|
// TODO: month selection dropdown menu
|
||||||
|
|
||||||
|
const Calendar = () => {
|
||||||
|
const [monthIndex, setMonthIndex] = useState(0);
|
||||||
|
const [currentYear, setCurrentYear] = useState(new Date().getFullYear());
|
||||||
|
|
||||||
|
const changeMonth = (delta: number) => {
|
||||||
|
setMonthIndex((prev) => {
|
||||||
|
const newIndex = prev + delta;
|
||||||
|
if (newIndex > 11) {
|
||||||
|
setCurrentYear((y) => y + 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (newIndex < 0) {
|
||||||
|
setCurrentYear((y) => y - 1);
|
||||||
|
return 11;
|
||||||
|
}
|
||||||
|
return newIndex;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseBackground>
|
||||||
|
<CalendarHeader
|
||||||
|
changeMonth={changeMonth}
|
||||||
|
monthIndex={monthIndex}
|
||||||
|
currentYear={currentYear}
|
||||||
|
/>
|
||||||
|
<WeekDaysLine />
|
||||||
|
<CalendarGrid month={MONTHS[monthIndex]} year={currentYear} />
|
||||||
|
</BaseBackground>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type MonthSelectorProps = {
|
||||||
|
modalVisible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
position: { top: number; left: number; width: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
const MonthSelector = ({
|
||||||
|
modalVisible,
|
||||||
|
onClose,
|
||||||
|
position,
|
||||||
|
}: MonthSelectorProps) => {
|
||||||
|
const heightAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
type ItemType = { id: string; text: string };
|
||||||
|
const listRef = useRef<React.ComponentRef<typeof FlashList<ItemType>>>(null);
|
||||||
|
const [monthSelectorData, setMonthSelectorData] = useState(() => {
|
||||||
|
const initial = [];
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
initial.push({ id: i.toString(), text: `number ${i}` });
|
||||||
|
}
|
||||||
|
return initial;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appendToTestData = (
|
||||||
|
startIndex: number,
|
||||||
|
numberOfEntries: number,
|
||||||
|
appendToStart: boolean,
|
||||||
|
) => {
|
||||||
|
// create new data
|
||||||
|
const newData = [];
|
||||||
|
for (let i = 0; i < numberOfEntries; i++) {
|
||||||
|
const newIndex = startIndex + i + 1;
|
||||||
|
const newEntry = {
|
||||||
|
id: newIndex + "",
|
||||||
|
text: `number ${newIndex}`,
|
||||||
|
};
|
||||||
|
if (appendToStart) {
|
||||||
|
newData.unshift(newEntry);
|
||||||
|
} else {
|
||||||
|
newData.push(newEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// add new data
|
||||||
|
if (appendToStart) {
|
||||||
|
setMonthSelectorData([...newData, ...monthSelectorData]);
|
||||||
|
} else {
|
||||||
|
setMonthSelectorData([...monthSelectorData, ...newData]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (modalVisible) {
|
||||||
|
Animated.timing(heightAnim, {
|
||||||
|
toValue: 200,
|
||||||
|
duration: 200,
|
||||||
|
useNativeDriver: false,
|
||||||
|
}).start();
|
||||||
|
} else {
|
||||||
|
// reset on close
|
||||||
|
heightAnim.setValue(0);
|
||||||
|
}
|
||||||
|
}, [modalVisible]);
|
||||||
|
|
||||||
|
const renderItem = ({ item }: { item: ItemType }) => (
|
||||||
|
<Pressable>
|
||||||
|
<View className="w-full flex justify-center items-center">
|
||||||
|
<Text className="text-3xl">{item.text}</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={modalVisible}
|
||||||
|
transparent={true}
|
||||||
|
animationType="none"
|
||||||
|
onRequestClose={onClose}
|
||||||
|
>
|
||||||
|
<Pressable className="flex-1" onPress={onClose}>
|
||||||
|
<Animated.View
|
||||||
|
className="absolute bg-white border-2 border-solid rounded-lg overflow-hidden"
|
||||||
|
style={{
|
||||||
|
top: position.top,
|
||||||
|
left: position.left,
|
||||||
|
width: position.width,
|
||||||
|
height: heightAnim,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FlashList
|
||||||
|
className="w-full"
|
||||||
|
ref={listRef}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
data={monthSelectorData}
|
||||||
|
initialScrollIndex={5}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
onEndReached={() =>
|
||||||
|
appendToTestData(monthSelectorData.length, 10, false)
|
||||||
|
}
|
||||||
|
onStartReachedThreshold={0.5}
|
||||||
|
onStartReached={() =>
|
||||||
|
appendToTestData(monthSelectorData.length, 10, true)
|
||||||
|
}
|
||||||
|
renderItem={renderItem}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
</Pressable>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CalendarHeaderProps = {
|
||||||
|
changeMonth: (delta: number) => void;
|
||||||
|
monthIndex: number;
|
||||||
|
currentYear: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CalendarHeader = (props: CalendarHeaderProps) => {
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [dropdownPosition, setDropdownPosition] = useState({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
const containerRef = useRef<View>(null);
|
||||||
|
|
||||||
|
const prevMonth = () => props.changeMonth(-1);
|
||||||
|
const nextMonth = () => props.changeMonth(1);
|
||||||
|
|
||||||
|
const measureAndOpen = () => {
|
||||||
|
containerRef.current?.measureInWindow((x, y, width, height) => {
|
||||||
|
setDropdownPosition({ top: y + height, left: x, width });
|
||||||
|
setModalVisible(true);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Header className="flex flex-row items-center justify-between">
|
||||||
|
<ChangeMonthButton onPress={prevMonth} title={"<"} />
|
||||||
|
<View
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative flex flex-row items-center justify-around"
|
||||||
|
>
|
||||||
|
<Text className="text-4xl">
|
||||||
|
{MONTHS[props.monthIndex]} {props.currentYear}
|
||||||
|
</Text>
|
||||||
|
<Pressable
|
||||||
|
className={
|
||||||
|
"flex justify-center items-center bg-white w-12 h-12 p-2 " +
|
||||||
|
"border border-solid rounded-full ml-2"
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
borderColor: currentTheme.primeFg,
|
||||||
|
}}
|
||||||
|
onPress={measureAndOpen}
|
||||||
|
>
|
||||||
|
<Text className="text-4xl">v</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<MonthSelector
|
||||||
|
modalVisible={modalVisible}
|
||||||
|
onClose={() => setModalVisible(false)}
|
||||||
|
position={dropdownPosition}
|
||||||
|
/>
|
||||||
|
<ChangeMonthButton onPress={nextMonth} title={">"} />
|
||||||
|
</Header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type ChangeMonthButtonProps = {
|
||||||
|
onPress: () => void;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ChangeMonthButton = (props: ChangeMonthButtonProps) => (
|
||||||
|
<Pressable
|
||||||
|
onPress={props.onPress}
|
||||||
|
className={
|
||||||
|
"w-16 h-16 bg-white rounded-full flex items-center " +
|
||||||
|
"justify-center border border-solid border-1 mx-2"
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
borderColor: currentTheme.primeFg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text className="text-4xl">{props.title}</Text>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
|
||||||
|
const WeekDaysLine = () => (
|
||||||
|
<View className="flex flex-row items-center justify-around px-2 gap-2">
|
||||||
|
{/* TODO: px and gap need fine tuning to perfectly align with the grid */}
|
||||||
|
{DAYS.map((day, i) => (
|
||||||
|
<Text key={i}>{day.substring(0, 2).toUpperCase()}</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
|
||||||
|
type CalendarGridProps = {
|
||||||
|
month: Month;
|
||||||
|
year: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CalendarGrid = (props: CalendarGridProps) => {
|
||||||
|
const { baseDate, dateOffset } = useMemo(() => {
|
||||||
|
const monthIndex = MONTHS.indexOf(props.month);
|
||||||
|
const base = new Date(props.year, monthIndex, 1);
|
||||||
|
const offset = base.getDay() === 0 ? 6 : base.getDay() - 1;
|
||||||
|
return { baseDate: base, dateOffset: offset };
|
||||||
|
}, [props.month, props.year]);
|
||||||
|
|
||||||
|
// TODO: create array beforehand in a useMemo
|
||||||
|
const createDateFromOffset = (offset: number): Date => {
|
||||||
|
const date = new Date(baseDate);
|
||||||
|
date.setDate(date.getDate() + offset);
|
||||||
|
return date;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
className="h-full flex-1 flex-col flex-wrap gap-2 p-2"
|
||||||
|
style={{
|
||||||
|
backgroundColor: currentTheme.calenderBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
|
<View
|
||||||
|
key={i}
|
||||||
|
className="w-full flex-1 flex-row justify-around items-center gap-2"
|
||||||
|
>
|
||||||
|
{Array.from({ length: 7 }).map((_, j) => (
|
||||||
|
<SingleDay
|
||||||
|
key={j}
|
||||||
|
date={createDateFromOffset(i * 7 + j - dateOffset)}
|
||||||
|
month={props.month}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type SingleDayProps = {
|
||||||
|
date: Date;
|
||||||
|
month: Month;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SingleDay = (props: SingleDayProps) => {
|
||||||
|
const isSameMonth = MONTHS[props.date.getMonth()] === props.month;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
className="h-full flex-1 aspect-auto rounded-xl items-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: currentTheme.primeBg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
className={`text-xl ` + (isSameMonth ? "text-black" : "text-black/50")}
|
||||||
|
>
|
||||||
|
{props.date.getDate()}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Calendar;
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { View, Text, FlatList, TextInput } from "react-native";
|
import { View, Text, TextInput } from "react-native";
|
||||||
import currentTheme from "../Themes";
|
import currentTheme from "../Themes";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Header from "../components/Header";
|
import Header from "../components/Header";
|
||||||
import BaseBackground from "../components/BaseBackground";
|
import BaseBackground from "../components/BaseBackground";
|
||||||
|
import { FlashList } from "@shopify/flash-list";
|
||||||
|
|
||||||
// TODO: better shadows for everything
|
// TODO: better shadows for everything
|
||||||
// (maybe with extra library because of differences between android and ios)
|
// (maybe with extra library because of differences between android and ios)
|
||||||
@@ -246,8 +247,7 @@ const Chat = () => {
|
|||||||
return (
|
return (
|
||||||
<BaseBackground>
|
<BaseBackground>
|
||||||
<ChatHeader />
|
<ChatHeader />
|
||||||
<FlatList
|
<FlashList
|
||||||
inverted
|
|
||||||
data={messages}
|
data={messages}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
@@ -256,6 +256,10 @@ const Chat = () => {
|
|||||||
height={item.height}
|
height={item.height}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
maintainVisibleContentPosition={{
|
||||||
|
autoscrollToBottomThreshold: 0.2,
|
||||||
|
startRenderingFromBottom: true,
|
||||||
|
}}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
// extraData={selectedId} might need this later for re-rendering
|
// extraData={selectedId} might need this later for re-rendering
|
||||||
/>
|
/>
|
||||||
19
apps/client/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".expo/types/**/*.ts",
|
||||||
|
"expo-env.d.ts",
|
||||||
|
"nativewind-env.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
1
apps/server/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dist
|
||||||
19
apps/server/package.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "@caldav/server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "tsx watch src/app.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/app.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@caldav/shared": "*",
|
||||||
|
"express": "^5.2.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"tsx": "^4.21.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
apps/server/src/app.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const port = 3000;
|
||||||
|
|
||||||
|
app.get('/', (req: Request, res: Response) => {
|
||||||
|
res.send('Hello World!');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Example app listening on port ${port}`);
|
||||||
|
});
|
||||||
0
apps/server/src/server.ts
Normal file
13
apps/server/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "CommonJS",
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 384 KiB |
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1,10 +1,5 @@
|
|||||||
// https://docs.expo.dev/guides/using-eslint/
|
// https://docs.expo.dev/guides/using-eslint/
|
||||||
const { defineConfig } = require('eslint/config');
|
const { defineConfig } = require('eslint/config');
|
||||||
const expoConfig = require('eslint-config-expo/flat');
|
|
||||||
|
|
||||||
module.exports = defineConfig([
|
module.exports = defineConfig([
|
||||||
expoConfig,
|
|
||||||
{
|
|
||||||
ignores: ['dist/*'],
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
|||||||
2904
package-lock.json
generated
51
package.json
@@ -1,50 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "caldav",
|
"name": "caldav-mono",
|
||||||
"main": "expo-router/entry",
|
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"scripts": {
|
"private": true,
|
||||||
"start": "expo start",
|
"workspaces": [
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"apps/*",
|
||||||
"android": "expo start --android",
|
"packages/*"
|
||||||
"ios": "expo start --ios",
|
],
|
||||||
"web": "expo start --web",
|
|
||||||
"lint": "expo lint"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@expo/vector-icons": "^15.0.3",
|
|
||||||
"@react-navigation/bottom-tabs": "^7.4.0",
|
|
||||||
"@react-navigation/elements": "^2.6.3",
|
|
||||||
"@react-navigation/native": "^7.1.8",
|
|
||||||
"expo": "~54.0.25",
|
|
||||||
"expo-constants": "~18.0.10",
|
|
||||||
"expo-font": "~14.0.9",
|
|
||||||
"expo-haptics": "~15.0.7",
|
|
||||||
"expo-image": "~3.0.10",
|
|
||||||
"expo-linking": "~8.0.9",
|
|
||||||
"expo-router": "~6.0.15",
|
|
||||||
"expo-splash-screen": "~31.0.11",
|
|
||||||
"expo-status-bar": "~3.0.8",
|
|
||||||
"expo-symbols": "~1.0.7",
|
|
||||||
"expo-system-ui": "~6.0.8",
|
|
||||||
"expo-web-browser": "~15.0.9",
|
|
||||||
"nativewind": "^4.2.1",
|
|
||||||
"react": "19.1.0",
|
|
||||||
"react-dom": "19.1.0",
|
|
||||||
"react-native": "0.81.5",
|
|
||||||
"react-native-gesture-handler": "~2.28.0",
|
|
||||||
"react-native-reanimated": "~3.17.4",
|
|
||||||
"react-native-safe-area-context": "5.4.0",
|
|
||||||
"react-native-screens": "~4.16.0",
|
|
||||||
"react-native-web": "~0.21.0",
|
|
||||||
"react-native-worklets": "0.5.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.1.0",
|
|
||||||
"eslint": "^9.25.0",
|
"eslint": "^9.25.0",
|
||||||
"eslint-config-expo": "~10.0.0",
|
|
||||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
|
||||||
"tailwindcss": "^3.4.17",
|
|
||||||
"typescript": "~5.9.2"
|
"typescript": "~5.9.2"
|
||||||
},
|
}
|
||||||
"private": true
|
|
||||||
}
|
}
|
||||||
|
|||||||
8
packages/shared/package.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "@caldav/shared",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"exports": {
|
||||||
|
"./*": "./src/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/shared/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES2020",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { Pressable, Text, View } from "react-native";
|
|
||||||
import { DAYS, MONTHS, Month } from "../Constants";
|
|
||||||
import Header from "../components/Header";
|
|
||||||
import { useMemo, useState } from "react";
|
|
||||||
import currentTheme from "../Themes";
|
|
||||||
import BaseBackground from "../components/BaseBackground";
|
|
||||||
|
|
||||||
// TODO: month selection dropdown menu
|
|
||||||
|
|
||||||
const Calendar = () => {
|
|
||||||
const [monthIndex, setMonthIndex] = useState(0);
|
|
||||||
const [currentYear, setCurrentYear] = useState(new Date().getFullYear());
|
|
||||||
|
|
||||||
const changeMonth = (delta: number) => {
|
|
||||||
setMonthIndex((prev) => {
|
|
||||||
const newIndex = prev + delta;
|
|
||||||
if (newIndex > 11) {
|
|
||||||
setCurrentYear((y) => y + 1);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
if (newIndex < 0) {
|
|
||||||
setCurrentYear((y) => y - 1);
|
|
||||||
return 11;
|
|
||||||
}
|
|
||||||
return newIndex;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseBackground>
|
|
||||||
<CalendarHeader
|
|
||||||
changeMonth={changeMonth}
|
|
||||||
monthIndex={monthIndex}
|
|
||||||
currentYear={currentYear}
|
|
||||||
/>
|
|
||||||
<WeekDaysLine />
|
|
||||||
<CalendarGrid month={MONTHS[monthIndex]} year={currentYear} />
|
|
||||||
</BaseBackground>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type CalendarHeaderProps = {
|
|
||||||
changeMonth: (delta: number) => void;
|
|
||||||
monthIndex: number;
|
|
||||||
currentYear: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CalendarHeader = (props: CalendarHeaderProps) => {
|
|
||||||
const prevMonth = () => props.changeMonth(-1);
|
|
||||||
const nextMonth = () => props.changeMonth(1);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Header className="flex flex-row items-center justify-between">
|
|
||||||
<ChangeMonthButton onPress={prevMonth} title={"<"} />
|
|
||||||
<Text className="text-4xl">
|
|
||||||
{MONTHS[props.monthIndex]} {props.currentYear}
|
|
||||||
</Text>
|
|
||||||
<ChangeMonthButton onPress={nextMonth} title={">"} />
|
|
||||||
</Header>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChangeMonthButtonProps = {
|
|
||||||
onPress: () => void;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ChangeMonthButton = (props: ChangeMonthButtonProps) => (
|
|
||||||
<Pressable
|
|
||||||
onPress={props.onPress}
|
|
||||||
className={
|
|
||||||
"w-16 h-16 bg-white rounded-full flex items-center " +
|
|
||||||
"justify-center border border-solid border-1 mx-2"
|
|
||||||
}
|
|
||||||
style={{
|
|
||||||
borderColor: currentTheme.primeFg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text className="text-4xl">{props.title}</Text>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
|
|
||||||
const WeekDaysLine = () => (
|
|
||||||
<View className="flex flex-row items-center justify-around px-2 gap-2">
|
|
||||||
{/* TODO: px and gap need fine tuning to perfectly align with the grid */}
|
|
||||||
{DAYS.map((day, i) => (
|
|
||||||
<Text key={i}>{day.substring(0, 2).toUpperCase()}</Text>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
|
|
||||||
type CalendarGridProps = {
|
|
||||||
month: Month;
|
|
||||||
year: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
const CalendarGrid = (props: CalendarGridProps) => {
|
|
||||||
const { baseDate, dateOffset } = useMemo(() => {
|
|
||||||
const monthIndex = MONTHS.indexOf(props.month);
|
|
||||||
const base = new Date(props.year, monthIndex, 1);
|
|
||||||
const offset = base.getDay() === 0 ? 6 : base.getDay() - 1;
|
|
||||||
return { baseDate: base, dateOffset: offset };
|
|
||||||
}, [props.month, props.year]);
|
|
||||||
|
|
||||||
// TODO: create array beforehand in a useMemo
|
|
||||||
const createDateFromOffset = (offset: number): Date => {
|
|
||||||
const date = new Date(baseDate);
|
|
||||||
date.setDate(date.getDate() + offset);
|
|
||||||
return date;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
className="h-full flex-1 flex-col flex-wrap gap-2 p-2"
|
|
||||||
style={{
|
|
||||||
backgroundColor: currentTheme.calenderBg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
|
||||||
<View
|
|
||||||
key={i}
|
|
||||||
className="w-full flex-1 flex-row justify-around items-center gap-2"
|
|
||||||
>
|
|
||||||
{Array.from({ length: 7 }).map((_, j) => (
|
|
||||||
<SingleDay
|
|
||||||
key={j}
|
|
||||||
date={createDateFromOffset(i * 7 + j - dateOffset)}
|
|
||||||
month={props.month}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type SingleDayProps = {
|
|
||||||
date: Date;
|
|
||||||
month: Month;
|
|
||||||
};
|
|
||||||
|
|
||||||
const SingleDay = (props: SingleDayProps) => {
|
|
||||||
const isSameMonth = MONTHS[props.date.getMonth()] === props.month;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
className="h-full flex-1 aspect-auto rounded-xl items-center"
|
|
||||||
style={{
|
|
||||||
backgroundColor: currentTheme.primeBg,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
className={
|
|
||||||
`text-xl ` + (isSameMonth ? "text-black" : "text-black/50")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.date.getDate()}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Calendar;
|
|
||||||
@@ -1,18 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "expo/tsconfig.base",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"strict": true,
|
"strict": true
|
||||||
"paths": {
|
|
||||||
"@/*": [
|
|
||||||
"./*"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"include": [
|
"references": [
|
||||||
"**/*.ts",
|
{ "path": "packages/shared" },
|
||||||
"**/*.tsx",
|
{ "path": "apps/client" },
|
||||||
".expo/types/**/*.ts",
|
{ "path": "apps/server" }
|
||||||
"expo-env.d.ts",
|
|
||||||
"nativewind-env.d.ts"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||