PapillonApp / Papillon

Une alternative open source aux applis de vie scolaire. L'allié de tous les étudiants. Le futur de l'éducation numérique libre et ouverte.
https://papillon.bzh
GNU General Public License v3.0
112 stars 42 forks source link

feat: Papillon 100% hors connexion #346

Open Kgeek33 opened 2 weeks ago

Kgeek33 commented 2 weeks ago

Participation de @imyanice dans cette PR

🚀 Nouvelle Pull Request

Proposez vos modifications pour améliorer Papillon

Informations importantes

Merci de vous référer à la documentation sur la contribution si vous avez des questions à propos des pull requests (https://gitbook.getpapillon.xyz/organisation/outils-internes/github)

Checklist d'avant pull request

Veuillez cocher toutes les cases applicables en remplaçant [ ] par [x].

Changelogs proposés

L'application est disponible à 100% hors connexion ! En fonction de la page, soit est affiché une icône WifiOff, soit Reanimated.View avec un titre au hasard et un texte disant que l'utilisateur est en mode hors connexion

Issues en lien

Informations supplémentaires

[!WARNING]

Quand l'utilisateur est en mode hors connexion, et qu'il se reconnecte, il y aura un chargement car Papillon n'aura pas réussi à se connecter. J'ai cherché des moyens pour raffraichir des pages, sans succès

Il faut redémarrer l'app pour que ça fonctionne comme attendu

Captures d'écran/Vidéos

Page Image/Vidéo
Home Home
Devoirs Devoirs
Cocher un devoir (même coche pour mettre une actualité en lu/non lu) https://github.com/user-attachments/assets/3b329a5f-9cfe-45ba-942a-f4c613df26ed
Emploi du temps Lessons
Notes Notes
Actualités Actualités
Cantine (pareil pour Messages) Cantine
Ajouter un compte https://github.com/user-attachments/assets/b5d9f830-3745-42a8-b9ad-7f4a7e583fb2
camarm-dev commented 2 weeks ago

Essaye de faire passer le linter / typecheck ma PR a été merge... Je suis en train de tester mais ça charge dans le vide expo go là

camarm-dev commented 2 weeks ago

Ah nan ma PR est pas merge mb

Kgeek33 commented 2 weeks ago

non ta pr n'est pas passé, mais t'as des erreurs de typages dans ta pr 🤣

camarm-dev commented 2 weeks ago

non ta pr n'est pas passé, mais t'as des erreurs de typages dans ta pr 🤣

Nan elle est clean ma PR

Kgeek33 commented 2 weeks ago

il y a 50 min, vince a fait un commit sur ta pr qui fait que t'as des erreurs de typages désormais

camarm-dev commented 2 weeks ago

oh nan j'vais pas vu pourquoi

Kgeek33 commented 2 weeks ago

Il a corrigé des bugs mais j'ai pas vu en détail pourquoi des erreurs de typage

imyanice commented 2 weeks ago

Perso je trouve qu'il y'a beaucoup trop d'indicateur hors connexion et c'est moche, de plus entre Messages et Notes c'est pas centré pareil.

image imageimageimage

imyanice commented 2 weeks ago

Je pense que l'on peut enlever l'indicateur rouge et juste garder le header "Flûte"/"Catastrophe" sur chaque page, je trouve ça moins agressif...

imyanice commented 2 weeks ago

De plus, les chats ne sont pas disponibles hors connexion

Kgeek33 commented 2 weeks ago

ok je corrige ça 👍, je laisse le wifi en rouge sur les devoirs ou pas ?

Ah oui, j'ai oublié de changer le texte pour le chat 😅

imyanice commented 2 weeks ago

ok je corrige ça 👍, je laisse le wifi en rouge sur les devoirs ou pas ?

Je ne pense pas que ça soit utile! Rajoute juste le header Flûte! ça suffit imo

Sinon very cool la pr 😄

Kgeek33 commented 2 weeks ago

ok je fais ça haha merci 😃

Kgeek33 commented 2 weeks ago

@imyanice c'est bon pour moi j'ajoute sur l'emploi du temps et tout est parfait

Kgeek33 commented 2 weeks ago

Tout est bon pour moi, à vous de review :)

imyanice commented 2 weeks ago

imageimageimage

imyanice commented 2 weeks ago

Voici mon review qualitatif, t'as un problème de rendu et les chats ne sont pas hors ligne (je crois que c'est qq chose que tu as rajouté ?)

Kgeek33 commented 2 weeks ago
imyanice commented 2 weeks ago

D'acc! Super!! Je te mets une vidéo si ça t'aide pour debug le truc https://github.com/user-attachments/assets/9973687e-9446-44cb-b3f5-52ff3b2cbdfc

imyanice commented 2 weeks ago

Seules les notes ont l'air de fonctionner

Kgeek33 commented 2 weeks ago

Mdrr je l'avais pas vu venir le défilement de la banderole dans les devoirs 🤣🤣

Kgeek33 commented 2 weeks ago

@imyanice c'est bon pour moi !

imyanice commented 2 weeks ago

Y'a toujours des erreurs de rendu :) imageimage

imyanice commented 2 weeks ago

Il faudrait que ça défile avec le reste, là ce n'est pas le cas

Kgeek33 commented 2 weeks ago

J'aurai bien aimé que ça défile mais le problème, c'est que tous les devoirs sont affichés via le composant <FlatList>

Kgeek33 commented 2 weeks ago

À la limite, j'pensais a ça. Afficher une bulle sur le header indiquant "Mode hors connexion" En orange, la où ça devrait être placé

Screenshot_2024-11-10-21-25-52-266_xyz.getpapillon.app-edit.jpg

Screenshot_2024-11-10-21-25-34-769_xyz.getpapillon.app-edit.jpg

Kgeek33 commented 2 weeks ago

Comme le Semaine 11, mais marqué comme "Hors connexion" avec une couleur rouge (ou non)

Kgeek33 commented 2 weeks ago

genre comme ça par exemple pour les devoirs :

Rouge En fonction du système
1731271207858 1731271207861
imyanice commented 2 weeks ago
import { NativeItem, NativeList, NativeListHeader, NativeText } from "@/components/Global/NativeComponents";
import { useCurrentAccount } from "@/stores/account";
import { useHomeworkStore } from "@/stores/homework";
import { useTheme } from "@react-navigation/native";
import React, { useRef, useState, useCallback, useEffect, useMemo } from "react";
import { toggleHomeworkState, updateHomeworkForWeekInCache } from "@/services/homework";
import {
  View,
  Text,
  FlatList,
  Dimensions,
  Button,
  ScrollView,
  RefreshControl,
  StyleSheet,
  ActivityIndicator,
  TextInput,
  ListRenderItem
} from "react-native";
import { dateToEpochWeekNumber, epochWNToDate } from "@/utils/epochWeekNumber";

import HomeworksNoHomeworksItem from "./Atoms/NoHomeworks";
import HomeworkItem from "./Atoms/Item";
import { PressableScale } from "react-native-pressable-scale";
import { TouchableOpacity } from "react-native-gesture-handler";
import { Book, Check, CheckCircle, CheckCircle2, CheckSquare, ChevronLeft, ChevronRight, CircleDashed, CircleDotDashed, Search, WifiOff, X } from "lucide-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { BlurView } from "expo-blur";

import Reanimated, { Easing, FadeIn, FadeInLeft, FadeInRight, FadeInUp, FadeOut, FadeOutDown, FadeOutLeft, FadeOutRight, FadeOutUp, FlipInXDown, LinearTransition, ZoomIn, ZoomOut } from "react-native-reanimated";
import { animPapillon } from "@/utils/ui/animations";
import PapillonSpinner from "@/components/Global/PapillonSpinner";
import AnimatedNumber from "@/components/Global/AnimatedNumber";
import { LinearGradient } from "expo-linear-gradient";
import * as Haptics from "expo-haptics";
import MissingItem from "@/components/Global/MissingItem";
import { PapillonModernHeader } from "@/components/Global/PapillonModernHeader";
import {Homework} from "@/services/shared/Homework";
import {Account} from "@/stores/account/types";
import {Screen} from "@/router/helpers/types";
import {NativeSyntheticEvent} from "react-native/Libraries/Types/CoreEventTypes";
import {NativeScrollEvent, ScrollViewProps} from "react-native/Libraries/Components/ScrollView/ScrollView";
import {SearchBar} from "react-native-screens";
import NetInfo from "@react-native-community/netinfo";
import { getErrorTitle } from "@/utils/format/get_papillon_error_title";

type HomeworksPageProps = {
  index: number;
  isActive: boolean;
  loaded: boolean;
  homeworks: Record<number, Homework[]>;
  account: Account;
  updateHomeworks: () => Promise<void>;
  loading: boolean;
  getDayName: (date: string | number | Date) => string;
};

const formatDate = (date: string | number | Date): string => {
  return new Date(date).toLocaleDateString("fr-FR", {
    day: "numeric",
    month: "long"
  });
};

const WeekView: Screen<"Homeworks"> = ({ route, navigation }) => {
  const flatListRef: React.MutableRefObject<FlatList> = useRef(null) as any as React.MutableRefObject<FlatList>;
  const { width } = Dimensions.get("window");
  const finalWidth = width - (width > 600 ? (
    320 > width * 0.35 ? width * 0.35 :
      320
  ) : 0);
  const insets = useSafeAreaInsets();

  const outsideNav = route.params?.outsideNav;

  const theme = useTheme();
  const account = useCurrentAccount(store => store.account!);
  const homeworks = useHomeworkStore(store => store.homeworks);

  // @ts-expect-error
  let firstDate = account?.instance?.instance?.firstDate || null;
  if (!firstDate) {
    firstDate = new Date();
    firstDate.setMonth(8);
    firstDate.setDate(1);
  }
  const firstDateEpoch = dateToEpochWeekNumber(firstDate);

  // Function to get the current week number since epoch
  const getCurrentWeekNumber = () => {
    const now = new Date();
    now.setHours(0, 0, 0, 0);
    const start = new Date(1970, 0, 0);
    start.setHours(0, 0, 0, 0);
    const diff = now.getTime() - start.getTime();
    const oneWeek = 1000 * 60 * 60 * 24 * 7;
    return Math.floor(diff / oneWeek) + 1;
  };

  const currentWeek = getCurrentWeekNumber();
  const [data, setData] = useState(Array.from({ length: 100 }, (_, i) => currentWeek - 50 + i));

  const [selectedWeek, setSelectedWeek] = useState(currentWeek);
  const [direction, setDirection] = useState<"left" | "right">("right");
  const [oldSelectedWeek, setOldSelectedWeek] = useState(selectedWeek);

  const [hideDone, setHideDone] = useState(false);

  const getItemLayout = useCallback((_: any, index: number) => ({
    length: finalWidth,
    offset: finalWidth * index,
    index,
  }), [width]);

  const keyExtractor = useCallback((item: any) => item.toString(), []);

  const getDayName = (date: string | number | Date): string => {
    const days = ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
    return days[new Date(date).getDay()];
  };

  const errorTitle = useMemo(() => getErrorTitle(), []);
  const [loading, setLoading] = useState(false);
  const [refreshing, setRefreshing] = useState(false);

  const [loadedWeeks, setLoadedWeeks] = useState<number[]>([]);

  const [isOnline, setIsOnline] = useState(true);
  useEffect(() => {
    return NetInfo.addEventListener(state => {
      setIsOnline(state.isConnected ?? false);
    });
  }, []);

  const updateHomeworks = useCallback(async (force = false, showRefreshing = true, showLoading = true) => {
    if(!account) return;

    if (!force && loadedWeeks.includes(selectedWeek)) {
      return;
    }

    if (showRefreshing) {
      setRefreshing(true);
    }
    if (showLoading) {
      setLoading(true);
    }
    console.log("[Homeworks]: updating cache...", selectedWeek, epochWNToDate(selectedWeek));
    updateHomeworkForWeekInCache(account, epochWNToDate(selectedWeek))
      .then(() => {
        console.log("[Homeworks]: updated cache !", epochWNToDate(selectedWeek));
        setLoading(false);
        setRefreshing(false);
        setLoadedWeeks(prev => [...prev, selectedWeek]);
      });
  }, [account, selectedWeek, loadedWeeks]);

  // on page change, load the homeworks
  useEffect(() => {
    if (selectedWeek > oldSelectedWeek) {
      setDirection("right");
    } else if (selectedWeek < oldSelectedWeek) {
      setDirection("left");
    }

    setTimeout(() => {
      setOldSelectedWeek(selectedWeek);
      updateHomeworks(false, false);
    }, 0);
  }, [selectedWeek]);

  const [searchTerms, setSearchTerms] = useState("");

  const renderWeek: ListRenderItem<number> = ({ item }) => {
    const homeworksInWeek = homeworks[item] ?? [];

    const sortedHomework = homeworksInWeek.sort((a, b) => new Date(a.due).getTime() - new Date(b.due).getTime());

    const groupedHomework = sortedHomework.reduce((acc, curr) => {
      const dayName = getDayName(curr.due);
      const formattedDate = formatDate(curr.due);
      const day = `${dayName} ${formattedDate}`;

      if (!acc[day]) {
        acc[day] = [curr];
      } else {
        acc[day].push(curr);
      }

      // filter homeworks by search terms
      if (searchTerms.length > 0) {
        acc[day] = acc[day].filter(homework => {
          const content = homework.content.toLowerCase().trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
          const subject = homework.subject.toLowerCase().trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
          return content.includes(searchTerms.toLowerCase().trim().normalize("NFD").replace(/[\u0300-\u036f]/g, "")) ||
                 subject.includes(searchTerms.toLowerCase().trim().normalize("NFD").replace(/[\u0300-\u036f]/g, ""));
        });
      }

      // if hideDone is enabled, filter out the done homeworks
      if (hideDone) {
        acc[day] = acc[day].filter(homework => !homework.done);
      }

      // remove all empty days
      if (acc[day].length === 0) {
        delete acc[day];
      }

      return acc;
    }, {} as Record<string, Homework[]>);

    // Moved completed homework to the bottom of the day
    const sortedGroupedHomework = Object.keys(groupedHomework).reduce((acc, day) => {
      acc[day] = groupedHomework[day].sort((a, b) => {
        if (a.done === b.done) {
          return 0; // Keep the current order if both are either completed or not completed
        }
        return a.done ? 1 : -1; // Unfinished at the top, finished at the bottom
      });
      return acc;
    }, {} as Record<string, Homework[]>);

    return (
      <ScrollView
        style={{ width: finalWidth, height: "100%" }}
        contentContainerStyle={{
          padding: 16,
          paddingTop: outsideNav ? 72 : insets.top + 56,
        }}
        showsVerticalScrollIndicator={false}
        refreshControl={
          <RefreshControl
            refreshing={refreshing}
            onRefresh={() => updateHomeworks(true)}
            progressViewOffset={outsideNav ? 72 : insets.top + 56}
          />
        }
      >

        {!isOnline &&
          <Reanimated.View
            entering={FlipInXDown.springify().mass(1).damping(20).stiffness(300)}
            exiting={FadeOutUp.springify().mass(1).damping(20).stiffness(300)}
            layout={animPapillon(LinearTransition)}
            style={{
              backgroundColor: theme.colors.background,
            }}
          >
            <NativeList inline>
              <NativeItem icon={<WifiOff />}>
                <NativeText variant="title" style={{ paddingVertical: 2, marginBottom: -4 }}>
                  {errorTitle.label} {errorTitle.emoji}
                </NativeText>
                <NativeText variant="subtitle">
                  Vous êtes hors ligne. Les données affichées peuvent être obsolètes.
                </NativeText>
              </NativeItem>
            </NativeList>
          </Reanimated.View>
        }

        {groupedHomework && Object.keys(groupedHomework).map((day, index) => (
          <Reanimated.View
            key={day}
            entering={animPapillon(FadeInUp)}
            exiting={animPapillon(FadeOutDown)}
            layout={animPapillon(LinearTransition)}
          >
            <NativeListHeader animated label={day} />

            <NativeList animated>
              {groupedHomework[day].map((homework, idx) => (
                <HomeworkItem
                  key={homework.id}
                  index={idx}
                  navigation={navigation}
                  total={groupedHomework[day].length}
                  homework={homework}
                  onDonePressHandler={async () => {
                    await toggleHomeworkState(account, homework);
                    await updateHomeworks(true, false, false);
                  }}
                />
              ))}
            </NativeList>
          </Reanimated.View>
        ))}

        {groupedHomework && Object.keys(groupedHomework).length === 0 &&
          <Reanimated.View
            style={{
              marginTop: 24,
              width: "100%",
            }}
            layout={animPapillon(LinearTransition)}
            key={searchTerms + hideDone}
          >
            {searchTerms.length > 0 ?
              <MissingItem
                emoji="🔍"
                title="Aucun résultat"
                description="Aucun devoir ne correspond à votre recherche."
              />
              :
              hideDone ?
                <MissingItem
                  emoji="🌴"
                  title="Il ne reste rien à faire"
                  description="Il n'y a aucun devoir non terminé pour cette semaine."
                />
                :
                <MissingItem
                  emoji="📚"
                  title="Aucun devoir"
                  description="Il n'y a aucun devoir pour cette semaine."
                />}
          </Reanimated.View>
        }
      </ScrollView>
    );
  };

  const onEndReached = () => {
    const lastWeek = data[data.length - 1];
    const newWeeks = Array.from({ length: 50 }, (_, i) => lastWeek + i + 1);
    setData(prevData => [...prevData, ...newWeeks]);
  };

  const onStartReached = () => {
    const firstWeek = data[0];
    const newWeeks = Array.from({ length: 50 }, (_, i) => firstWeek - 50 + i);
    setData(prevData => [...newWeeks, ...prevData]);
    flatListRef.current?.scrollToIndex({ index: 50, animated: false });
  };

  const onScroll: ScrollViewProps["onScroll"] = useCallback(({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
    if (nativeEvent.contentOffset.x < finalWidth) {
      onStartReached();
    }

    // Update selected week based on scroll position
    const index = Math.round(nativeEvent.contentOffset.x / finalWidth);
    setSelectedWeek(data[index]);
  }, [finalWidth, data]);

  const onMomentumScrollEnd: ScrollViewProps["onMomentumScrollEnd"] = useCallback(({ nativeEvent }: NativeSyntheticEvent<NativeScrollEvent>) => {
    const index = Math.round(nativeEvent.contentOffset.x / finalWidth);
    setSelectedWeek(data[index]);
  }, [finalWidth, data]);

  const goToWeek = useCallback((weekNumber: number) => {
    const index = data.findIndex(week => week === weekNumber);
    if (index !== -1) {
      // @ts-expect-error
      const currentIndex = Math.round(flatListRef.current?.contentOffset?.x / finalWidth) || 0;
      const distance = Math.abs(index - currentIndex);
      const animated = distance <= 10; // Animate if the distance is 10 weeks or less

      flatListRef.current?.scrollToIndex({ index, animated });
      setSelectedWeek(weekNumber);
    } else {
      // If the week is not in the current data, update the data and scroll
      const newData = Array.from({ length: 100 }, (_, i) => weekNumber - 50 + i);
      setData(newData);

      // Use a timeout to ensure the FlatList has updated before scrolling
      setTimeout(() => {
        flatListRef.current?.scrollToIndex({ index: 50, animated: false });
        setSelectedWeek(weekNumber);
      }, 0);
    }
  }, [data, finalWidth]);

  const [showPickerButtons, setShowPickerButtons] = useState(false);
  const [searchHasFocus, setSearchHasFocus] = useState(false);

  const SearchRef: React.MutableRefObject<TextInput> = useRef(null) as any as React.MutableRefObject<TextInput>;

  return (
    <View>
      <PapillonModernHeader outsideNav={outsideNav}>
        {showPickerButtons && !searchHasFocus &&
          <Reanimated.View
            layout={animPapillon(LinearTransition)}
            entering={animPapillon(ZoomIn)}
            exiting={animPapillon(ZoomOut)}
          >
            <PressableScale
              onPress={() => goToWeek(selectedWeek - 1)}
              activeScale={0.8}
            >
              <BlurView
                style={[styles.weekButton, {
                  backgroundColor: theme.colors.primary + 16,
                }]}
                tint={theme.dark ? "dark" : "light"}
              >
                <ChevronLeft
                  size={24}
                  color={theme.colors.primary}
                  strokeWidth={2.5}
                />
              </BlurView>
            </PressableScale>
          </Reanimated.View>
        }

        {!searchHasFocus &&
        <Reanimated.View
          layout={animPapillon(LinearTransition)}
          entering={animPapillon(FadeIn).delay(100)}
          exiting={animPapillon(FadeOutLeft)}
        >
          <PressableScale
            style={[styles.weekPickerContainer]}
            onPress={() => setShowPickerButtons(!showPickerButtons)}
            onLongPress={() => {
              setHideDone(!hideDone);
              Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
            }}
            delayLongPress={200}
          >
            <Reanimated.View
              layout={animPapillon(LinearTransition)}
              style={[{
                backgroundColor:
                showPickerButtons ? theme.colors.primary + 16 :
                  theme.colors.text + 16,
                overflow: "hidden",
                borderRadius: 80,
              }]}
            >
              <BlurView
                style={[styles.weekPicker, {
                  backgroundColor: "transparent",
                }]}
                tint={theme.dark ? "dark" : "light"}
              >
                {showPickerButtons && !loading &&
                  <Reanimated.View
                    entering={animPapillon(FadeIn)}
                    exiting={animPapillon(FadeOut)}
                    style={{
                      marginRight: 2,
                    }}
                  >
                    <Book
                      color={showPickerButtons ? theme.colors.primary : theme.colors.text}
                      size={18}
                      strokeWidth={2.6}
                    />
                  </Reanimated.View>
                }

                {!showPickerButtons && hideDone &&
                    <Reanimated.View
                      entering={animPapillon(ZoomIn)}
                      exiting={animPapillon(FadeOut)}
                      style={{
                        marginRight: 2,
                      }}
                    >
                      <CircleDashed
                        color={showPickerButtons ? theme.colors.primary : theme.colors.text}
                        size={18}
                        strokeWidth={3}
                        opacity={0.7}
                      />
                    </Reanimated.View>
                }

                <Reanimated.Text style={[styles.weekPickerText, styles.weekPickerTextIntl,
                  {
                    color: showPickerButtons ? theme.colors.primary : theme.colors.text,
                  }
                ]}
                layout={animPapillon(LinearTransition)}
                >
                  {width > 370 ? "Semaine" : "sem."}
                </Reanimated.Text>

                <Reanimated.View
                  layout={animPapillon(LinearTransition)}
                >
                  <AnimatedNumber
                    value={((selectedWeek - firstDateEpoch % 52) % 52 + 1).toString()}
                    style={[styles.weekPickerText, styles.weekPickerTextNbr,
                      {
                        color: showPickerButtons ? theme.colors.primary : theme.colors.text,
                      }
                    ]}
                  />
                </Reanimated.View>

                {isOnline && loading && (
                  <PapillonSpinner
                    size={18}
                    color={
                      showPickerButtons
                        ? theme.colors.primary
                        : theme.colors.text
                    }
                    strokeWidth={2.8}
                    entering={animPapillon(ZoomIn)}
                    exiting={animPapillon(ZoomOut)}
                    style={{
                      marginLeft: 5,
                    }}
                  />
                )}
              </BlurView>
            </Reanimated.View>
          </PressableScale>
        </Reanimated.View>
        }

        {showPickerButtons && !searchHasFocus &&
          <Reanimated.View
            layout={animPapillon(LinearTransition)}
            entering={animPapillon(ZoomIn).delay(100)}
            exiting={animPapillon(FadeOutLeft)}
          >
            <PressableScale
              onPress={() => goToWeek(selectedWeek + 1)}
              activeScale={0.8}
            >
              <BlurView
                style={[styles.weekButton, {
                  backgroundColor: theme.colors.primary + 16,
                }]}
                tint={theme.dark ? "dark" : "light"}
              >
                <ChevronRight
                  size={24}
                  color={theme.colors.primary}
                  strokeWidth={2.5}
                />
              </BlurView>
            </PressableScale>
          </Reanimated.View>
        }

        {showPickerButtons && !searchHasFocus &&
          <Reanimated.View
            layout={animPapillon(LinearTransition)}
            style={{
              flex: 1
            }}
          />
        }

        {showPickerButtons && !searchHasFocus && width > 330 &&
        <Reanimated.View
          layout={animPapillon(LinearTransition)}
          entering={animPapillon(FadeInLeft).delay(100)}
          exiting={animPapillon(FadeOutLeft)}
          style={{
            alignItems: "center",
            justifyContent: "center",
            backgroundColor: hideDone ? theme.colors.primary : theme.colors.background + "ff",
            borderColor: theme.colors.border + "dd",
            borderWidth: 1,
            borderRadius: 800,
            height: 40,
            width: showPickerButtons ? 40 : null,
            minWidth: showPickerButtons ? 40 : null,
            maxWidth: showPickerButtons ? 40 : null,
            gap: 4,
            shadowColor: "#00000022",
            shadowOffset: { width: 0, height: 2 },
            shadowOpacity: 0.6,
            shadowRadius: 4,
          }}
        >
          <TouchableOpacity
            onPress={() => {
              setHideDone(!hideDone);
            }}
          >
            <CheckSquare
              size={20}
              color={hideDone ? "#fff" : theme.colors.text}
              strokeWidth={2.5}
              opacity={hideDone ? 1 : 0.7}
            />
          </TouchableOpacity>
        </Reanimated.View>
        }

        <Reanimated.View
          layout={
            LinearTransition.duration(250).easing(Easing.bezier(0.5, 0, 0, 1).factory())
          }
          style={{
            flexDirection: "row",
            alignItems: "center",
            justifyContent: "center",
            flex: 1,
            backgroundColor: theme.colors.background + "ff",
            borderColor: theme.colors.border + "dd",
            borderWidth: 1,
            borderRadius: 800,
            paddingHorizontal: 14,
            height: 40,
            width: showPickerButtons ? 40 : null,
            minWidth: showPickerButtons ? 40 : null,
            maxWidth: showPickerButtons ? 40 : null,
            gap: 4,
            shadowColor: "#00000022",
            shadowOffset: { width: 0, height: 2 },
            shadowOpacity: 0.6,
            shadowRadius: 4,
          }}
        >
          <TouchableOpacity
            onPress={() => {
              setShowPickerButtons(false);

              setTimeout(() => {
                // #TODO : change timeout method or duration
                SearchRef.current?.focus();
              }, 20);
            }}
          >
            <Search
              size={20}
              color={theme.colors.text}
              strokeWidth={2.5}
              opacity={0.7}
            />
          </TouchableOpacity>

          {!showPickerButtons &&
          <Reanimated.View
            layout={animPapillon(LinearTransition)}
            style={{
              flex: 1,
              height: "100%",
              overflow: "hidden",
              borderRadius: 80,
            }}
            entering={FadeIn.duration(250).delay(20)}
            exiting={FadeOut.duration(100)}
          >
            <TextInput
              placeholder={
                (hideDone && !searchHasFocus) ? "Non terminé" :
                  "Rechercher"
              }
              value={searchTerms}
              onChangeText={setSearchTerms}
              placeholderTextColor={theme.colors.text + "80"}
              style={{
                color: theme.colors.text,
                padding: 8,
                borderRadius: 80,
                fontFamily: "medium",
                fontSize: 16.5,
                flex: 1,
              }}
              onFocus={() => setSearchHasFocus(true)}
              onBlur={() => setSearchHasFocus(false)}
              ref={SearchRef}
            />
          </Reanimated.View>
          }

          {searchTerms.length > 0 && searchHasFocus &&
          <TouchableOpacity
            onPress={() => {
              setSearchTerms("");
            }}
          >
            <Reanimated.View
              layout={animPapillon(LinearTransition)}
              entering={FadeIn.duration(100)}
              exiting={FadeOut.duration(100)}
            >
              <X
                size={20}
                color={theme.colors.text}
                strokeWidth={2.5}
                opacity={0.7}
              />
            </Reanimated.View>
          </TouchableOpacity>
          }
        </Reanimated.View>
      </PapillonModernHeader>

      <FlatList
        ref={flatListRef}
        data={data}
        renderItem={renderWeek}
        keyExtractor={keyExtractor}
        horizontal
        pagingEnabled
        showsHorizontalScrollIndicator={false}
        initialNumToRender={3}
        maxToRenderPerBatch={5}
        windowSize={5}
        getItemLayout={getItemLayout}
        onEndReached={onEndReached}
        onEndReachedThreshold={0.1}
        onScroll={onScroll}
        onMomentumScrollEnd={onMomentumScrollEnd}
        scrollEventThrottle={16}
        initialScrollIndex={50}
        style={{
          height: "100%",
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  header: {
    paddingHorizontal: 16,
    paddingVertical: 8,
    position: "absolute",
    top: 0,
    left: 0,
  },

  weekPickerContainer: {},

  weekPicker: {
    flexDirection: "row",
    justifyContent: "center",
    alignItems: "center",
    paddingHorizontal: 20,
    height: 40,
    borderRadius: 80,
    gap: 6,
    backgroundColor: "rgba(0, 0, 0, 0.05)",
    alignSelf: "flex-start",
    overflow: "hidden",
  },

  weekPickerText: {
    zIndex: 10000,
  },

  weekPickerTextIntl: {
    fontSize: 14.5,
    fontFamily: "medium",
    opacity: 0.7,
  },

  weekPickerTextNbr: {
    fontSize: 16.5,
    fontFamily: "semibold",
    marginTop: -1.5,
  },

  weekButton: {
    overflow: "hidden",
    borderRadius: 80,
    height: 38,
    width: 38,
    justifyContent: "center",
    alignItems: "center",
  },
});

export default WeekView;
Kgeek33 commented 2 weeks ago

c'est ça qui permet d'afficher les devoirs

<FlatList
  ref={flatListRef}
  data={data}
  renderItem={renderWeek}
  keyExtractor={keyExtractor}
  horizontal
  pagingEnabled
  showsHorizontalScrollIndicator={false}
  initialNumToRender={3}
  maxToRenderPerBatch={5}
  windowSize={5}
  getItemLayout={getItemLayout}
  onEndReached={onEndReached}
  onEndReachedThreshold={0.1}
  onScroll={onScroll}
  onMomentumScrollEnd={onMomentumScrollEnd}
  scrollEventThrottle={16}
  initialScrollIndex={50}
  style={{
    height: "100%",
  }}
/>
imyanice commented 2 weeks ago

c'est ça qui permet d'afficher les devoirs

<FlatList
  ref={flatListRef}
  data={data}
  renderItem={renderWeek}
  keyExtractor={keyExtractor}
  horizontal
  pagingEnabled
  showsHorizontalScrollIndicator={false}
  initialNumToRender={3}
  maxToRenderPerBatch={5}
  windowSize={5}
  getItemLayout={getItemLayout}
  onEndReached={onEndReached}
  onEndReachedThreshold={0.1}
  onScroll={onScroll}
  onMomentumScrollEnd={onMomentumScrollEnd}
  scrollEventThrottle={16}
  initialScrollIndex={50}
  style={{
    height: "100%",
  }}
/>

Ce que je t'ai envoyé fonctionne, FlatList prend RenderWeek qui permet de render la semaine. C'est donc là que je mets mon bazar, l'edt arrive

Kgeek33 commented 2 weeks ago

T'en penses quoi de ça ? https://github.com/PapillonApp/Papillon/pull/346#issuecomment-2466910593 On a envoyé le message en même temps, t'as pas du le voir

imyanice commented 2 weeks ago

Comme tu veux, mais maintenant que c'est fait et fix je vois pas l'intérêt lol https://github.com/user-attachments/assets/b2714681-202e-443b-8a0c-caacabec69fe

imyanice commented 2 weeks ago

Papillon/src/views/account/Lessons/Atoms/Page.tsx

import { NativeItem, NativeList, NativeText } from "@/components/Global/NativeComponents";
import { useTheme } from "@react-navigation/native";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { ActivityIndicator, Image, Platform, RefreshControl as RNRefreshControl, ScrollView, Text, View } from "react-native";
import { TimetableItem } from "./Item";
import { createNativeWrapper } from "react-native-gesture-handler";

import Reanimated, {
  FadeInDown,
  FadeOut,
  FadeOutUp,
  FlipInXDown,
  LinearTransition
} from "react-native-reanimated";
import NetInfo from "@react-native-community/netinfo";
import { Activity, Sofa, Utensils, WifiOff } from "lucide-react-native";
import LessonsNoCourseItem from "./NoCourse";
import { Timetable, TimetableClass } from "@/services/shared/Timetable";
import { animPapillon } from "@/utils/ui/animations";
import LessonsLoading from "./Loading";
import MissingItem from "@/components/Global/MissingItem";
import { getErrorTitle } from "@/utils/format/get_papillon_error_title";

const RefreshControl = createNativeWrapper(RNRefreshControl, {
  disallowInterruption: true,
  shouldCancelWhenOutside: false,
});

const lz = (num: number) => (num < 10 ? `0${num}` : num);

const getDuration = (minutes: number): string => {
  const durationHours = Math.floor(minutes / 60);
  const durationRemainingMinutes = minutes % 60;
  return `${durationHours} h ${lz(durationRemainingMinutes)} min`;
};

interface PageProps {
  current: boolean
  date: Date
  day: TimetableClass[]
  loading: boolean
  paddingTop: number
  refreshAction: () => unknown
  weekExists: boolean
}

export const Page = ({ day, date, current, paddingTop, refreshAction, loading, weekExists }: PageProps) => {
  const errorTitle = useMemo(() => getErrorTitle(), []);
  const [isOnline, setIsOnline] = useState(true);
  const theme = useTheme();
  useEffect(() => {
    return NetInfo.addEventListener(state => {
      setIsOnline(state.isConnected ?? false);
    });
  }, []);
  return (
    <ScrollView
      style={{
        flex: 1,
        width: "100%",
        height: "100%",
      }}
      showsVerticalScrollIndicator={false}
      contentContainerStyle={{
        paddingTop: paddingTop
      }}
      refreshControl={
        <RefreshControl
          refreshing={loading}
          onRefresh={refreshAction}
          progressViewOffset={paddingTop}
        />
      }
    >
      {current &&
        <View
          style={{
            paddingHorizontal: 10,
            paddingVertical: 10,
            gap: 10,
            width: "100%"
          }}
        >
          {!isOnline &&
          <Reanimated.View
            entering={FlipInXDown.springify().mass(1).damping(20).stiffness(300)}
            exiting={FadeOutUp.springify().mass(1).damping(20).stiffness(300)}
            layout={animPapillon(LinearTransition)}
            style={{
              backgroundColor: theme.colors.background,
            }}
          >
            <NativeList inline>
              <NativeItem icon={<WifiOff />}>
                <NativeText variant="title" style={{ paddingVertical: 2, marginBottom: -4 }}>
                  {errorTitle.label} {errorTitle.emoji}
                </NativeText>
                <NativeText variant="subtitle">
                  Vous êtes hors ligne. Les données affichées peuvent être obsolètes.
                </NativeText>
              </NativeItem>
            </NativeList>
          </Reanimated.View>
          }
          {day && day.length > 0 && day[0].type !== "vacation" && day.map((item, i) => (
            <View key={item.startTimestamp + i.toString()} style={{ gap: 10 }}>
              <TimetableItem key={item.startTimestamp} item={item} index={i} />

              {day[i + 1] &&
                day[i + 1].startTimestamp - item.endTimestamp > 1740000 && (
                <SeparatorCourse
                  i={i}
                  start={item.endTimestamp}
                  end={day[i + 1].startTimestamp}
                />
              )}
            </View>
          ))}
        </View>
      }

      {loading && day.length == 0 && (
        <Reanimated.View
          style={{
            padding: 26,
          }}
          entering={animPapillon(FadeInDown)}
          exiting={animPapillon(FadeOutUp).delay(100)}
        >
          <LessonsLoading />
        </Reanimated.View>
      )}

      {day && day.length === 0 && current && !loading && (
        weekExists && (new Date(date).getDay() == 6 || new Date(date).getDay() == 0) ? (
          <MissingItem
            emoji="🌴"
            title="C'est le week-end !"
            description="Profitez de votre week-end, il n'y a pas de cours aujourd'hui."
            entering={animPapillon(FadeInDown)}
            exiting={animPapillon(FadeOut)}
          />
        ) : (
          <MissingItem
            emoji="📆"
            title="Pas de cours aujourd'hui"
            description="Aucun cours n'est prévu pour aujourd'hui."
            entering={animPapillon(FadeInDown)}
            exiting={animPapillon(FadeOut)}
          />
        )
      )}

      {day.length === 1 && current && !loading && (day[0].type === "vacation" ? <MissingItem
        emoji="🏝️"
        title="C'est les vacances !"
        description="Profitez de vos vacances, à bientôt."
        entering={animPapillon(FadeInDown)}
        exiting={animPapillon(FadeOut)}
      />: <></>
      )}
    </ScrollView>
  );
};

const SeparatorCourse: React.FC<{
  i: number
  start: number
  end: number
}> = ({ i, start, end }) => {
  const { colors } = useTheme();
  const startHours = new Date(start).getHours();
  return (
    <Reanimated.View
      style={{
        borderRadius: 10,
        backgroundColor: colors.card,
        borderColor: colors.text + "33",
        borderWidth: 0.5,
        shadowColor: "#000",
        shadowOffset: { width: 0, height: 1 },
        shadowOpacity: 0.1,
        shadowRadius: 1,
        elevation: 1,
        marginLeft: 70,
      }}
      entering={
        Platform.OS === "ios" ?
          FadeInDown.delay(50 * i)
            .springify()
            .mass(1)
            .damping(20)
            .stiffness(300)
          : void 0
      }
      exiting={Platform.OS === "ios" ? FadeOut.duration(300) : void 0}
    >
      <View
        style={{
          flexDirection: "row",
          alignItems: "center",
          padding: 10,
          borderRadius: 10,
          gap: 10,
          overflow: "hidden",
          backgroundColor: colors.text + "11",
        }}
      >
        <Image
          source={require("../../../../../assets/images/mask_course.png")}
          resizeMode='cover'
          tintColor={colors.text}
          style={{
            position: "absolute",
            top: "-15%",
            left: "-20%",
            width: "200%",
            height: "300%",
            opacity: 0.05,
          }}
        />

        {startHours > 11 &&
          startHours < 14 ? (
            <Utensils size={20} color={colors.text} />
          ) : (
            <Sofa size={20} color={colors.text} />
          )}
        <Text
          numberOfLines={1}
          style={{
            flex: 1,
            fontFamily: "semibold",
            fontSize: 16,
            color: colors.text,
          }}
        >
          {startHours > 11 &&
            startHours < 14
            ? "Pause méridienne"
            : "Pas de cours"}
        </Text>

        <Text
          numberOfLines={1}
          style={{
            fontFamily: "medium",
            fontSize: 15,
            opacity: 0.5,
            color: colors.text,
          }}
        >
          {getDuration(
            Math.round((end - start) / 60000)
          )}
        </Text>
      </View>
    </Reanimated.View>
  );
};
Kgeek33 commented 2 weeks ago

super merci, j'intègre ça dans ma pr :)

imyanice commented 2 weeks ago

T'as pas enlevé le code ici, sinon à part ça lgtm!

image

LeGeek01 commented 2 weeks ago

y'a pas mal de pr assez sympa en terme de fonctionnalités mais les review sont pauvres, c'est dommage parce que ça traine et on peut pas avancer

Kgeek33 commented 2 weeks ago

Je corrige ça tout à l'heure @imyanice @LeGeek01 je suis d'accord, c'est pour ça qu'on travaille avec @Louis-htmlcss pour un workflow avec qr code, comme ça (et j'espère surtout), on devrait gagner énormément de temps

Kgeek33 commented 2 weeks ago

c bon @imyanice

ecnivtwelve commented 1 week ago

y'a pas mal de pr assez sympa en terme de fonctionnalités mais les review sont pauvres, c'est dommage parce que ça traine et on peut pas avancer

Ils sont vraiment incompétents les gens de Papillon dis donc

LeGeek01 commented 1 week ago

Sur certains points, oui, très clairement, notamment le manque de communication, point que j'ai souligné et rabaché et qui m'a coûté ma place au sein de l'équipe, et il n'y a pas d'amélioration.

Donc oui, à un moment, ça devient chiant.

tryon-dev commented 1 week ago

est ce que l'interface ressemble au screen qui ont été envoyé précédemment ?

tryon-dev commented 1 week ago

Sur certains points, oui, très clairement, notamment le manque de communication, point que j'ai souligné et rabaché et qui m'a coûté ma place au sein de l'équipe, et il n'y a pas d'amélioration.

Donc oui, à un moment, ça devient chiant.

Ca marche

Kgeek33 commented 1 week ago

@tryon-dev je mettrai des captures tout à l'heure, j'ai oublié d'en mettre comme elle était pas encore prête

Louis-htmlcss commented 1 week ago

@tryon-dev je mettrai des captures tout à l'heure, j'ai oublié d'en mettre comme elle était pas encore prête

Encore un avantage a notre pr qui permet une preview expo

Kgeek33 commented 1 week ago

@tryon-dev j'ai importé des vidéos et des captures dans la description de la pr :) Déso si les vidéos sont pas directement intégrés dans le tableau