mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-27 15:32:53 +02:00
🚚 feat(context): integrate context app into monorepo
Restructure the context app (formerly basetext) to follow the monorepo pattern with proper workspace configuration. Changes: - Move app files to apps/context/apps/mobile/ - Rename package to @context/mobile - Update bundle ID to com.manacore.context - Create pnpm-workspace.yaml for project workspace - Add dev scripts to root package.json - Update CLAUDE.md with project documentation The app structure is prepared for future web/backend additions. Note: Existing TypeScript errors in the original codebase are preserved. These should be fixed in a follow-up PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
34c879929b
commit
bb0e0cf5cb
303 changed files with 31904 additions and 475 deletions
37
apps/context/apps/mobile/utils/debounce.ts
Normal file
37
apps/context/apps/mobile/utils/debounce.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
export function debounce<T extends (...args: any[]) => any>(
|
||||
func: T,
|
||||
wait: number,
|
||||
options?: { leading?: boolean; trailing?: boolean }
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
let result: any;
|
||||
|
||||
const leading = options?.leading ?? false;
|
||||
const trailing = options?.trailing ?? true;
|
||||
|
||||
const debounced = function (this: any, ...args: Parameters<T>) {
|
||||
const context = this;
|
||||
const later = () => {
|
||||
timeout = null;
|
||||
if (trailing) result = func.apply(context, args);
|
||||
};
|
||||
|
||||
const callNow = leading && !timeout;
|
||||
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
|
||||
if (callNow) result = func.apply(context, args);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
debounced.cancel = () => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
};
|
||||
|
||||
return debounced;
|
||||
}
|
||||
44
apps/context/apps/mobile/utils/debug.ts
Normal file
44
apps/context/apps/mobile/utils/debug.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { ViewStyle } from 'react-native';
|
||||
|
||||
/**
|
||||
* Fügt Debug-Borders zu einem Style-Objekt hinzu
|
||||
* @param baseStyle Das Basis-Style-Objekt
|
||||
* @param color Die Farbe des Debug-Rahmens (optional)
|
||||
* @param showBorder Flag, ob der Debug-Rahmen angezeigt werden soll
|
||||
* @returns Das Style-Objekt mit Debug-Rahmen (wenn aktiviert)
|
||||
*/
|
||||
export const addDebugBorder = (
|
||||
baseStyle: ViewStyle,
|
||||
color: string = '#ff0000',
|
||||
showBorder: boolean = false
|
||||
): ViewStyle => {
|
||||
if (!showBorder) return baseStyle;
|
||||
|
||||
return {
|
||||
...baseStyle,
|
||||
borderWidth: 1,
|
||||
borderColor: color,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Generiert zufällige Debug-Farben für verschiedene UI-Elemente
|
||||
*/
|
||||
export const debugColors = {
|
||||
container: '#ff0000', // Rot
|
||||
section: '#00ff00', // Grün
|
||||
item: '#0000ff', // Blau
|
||||
header: '#ff00ff', // Magenta
|
||||
content: '#ffff00', // Gelb
|
||||
footer: '#00ffff', // Cyan
|
||||
|
||||
// Generiert eine zufällige Farbe für andere Elemente
|
||||
random: () => {
|
||||
const letters = '0123456789ABCDEF';
|
||||
let color = '#';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
color += letters[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return color;
|
||||
},
|
||||
};
|
||||
40
apps/context/apps/mobile/utils/eventEmitter.ts
Normal file
40
apps/context/apps/mobile/utils/eventEmitter.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Einfacher Event-Emitter für die App
|
||||
type EventCallback = (...args: any[]) => void;
|
||||
|
||||
class EventEmitter {
|
||||
private events: Record<string, EventCallback[]> = {};
|
||||
|
||||
// Event registrieren
|
||||
on(event: string, callback: EventCallback): void {
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
this.events[event].push(callback);
|
||||
}
|
||||
|
||||
// Event deregistrieren
|
||||
off(event: string, callback: EventCallback): void {
|
||||
if (!this.events[event]) return;
|
||||
this.events[event] = this.events[event].filter((cb) => cb !== callback);
|
||||
}
|
||||
|
||||
// Event auslösen
|
||||
emit(event: string, ...args: any[]): void {
|
||||
if (!this.events[event]) return;
|
||||
this.events[event].forEach((callback) => {
|
||||
try {
|
||||
callback(...args);
|
||||
} catch (error) {
|
||||
console.error(`Fehler beim Ausführen des Event-Handlers für ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton-Instanz
|
||||
export const eventEmitter = new EventEmitter();
|
||||
|
||||
// Event-Namen als Konstanten
|
||||
export const EVENTS = {
|
||||
TOKEN_BALANCE_UPDATED: 'TOKEN_BALANCE_UPDATED',
|
||||
};
|
||||
101
apps/context/apps/mobile/utils/i18n.ts
Normal file
101
apps/context/apps/mobile/utils/i18n.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import * as Localization from 'expo-localization';
|
||||
|
||||
// Import translation files
|
||||
import en from '~/locales/en.json';
|
||||
import de from '~/locales/de.json';
|
||||
|
||||
// Define supported languages
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
{ code: 'en', name: 'English', nativeName: 'English' },
|
||||
{ code: 'de', name: 'German', nativeName: 'Deutsch' },
|
||||
] as const;
|
||||
|
||||
export type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number]['code'];
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: en,
|
||||
},
|
||||
de: {
|
||||
translation: de,
|
||||
},
|
||||
};
|
||||
|
||||
// Get device language, fallback to English if not supported
|
||||
function getDeviceLanguage(): SupportedLanguage {
|
||||
try {
|
||||
const locale = Localization.locale;
|
||||
if (!locale) return 'en';
|
||||
|
||||
const languageCode = locale.split('-')[0] as SupportedLanguage;
|
||||
|
||||
// Check if the language is supported
|
||||
const isSupported = SUPPORTED_LANGUAGES.some((lang) => lang.code === languageCode);
|
||||
|
||||
return isSupported ? languageCode : 'en';
|
||||
} catch (error) {
|
||||
console.warn('Error getting device language:', error);
|
||||
return 'en';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize i18n
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: getDeviceLanguage(),
|
||||
fallbackLng: 'en',
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false, // React already escapes values
|
||||
},
|
||||
|
||||
// Enable plurals
|
||||
pluralSeparator: '_',
|
||||
|
||||
// Namespace configuration
|
||||
defaultNS: 'translation',
|
||||
|
||||
// Debug mode - set to true during development
|
||||
debug: __DEV__,
|
||||
|
||||
// React i18next options
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
// Helper function to get current language
|
||||
export const getCurrentLanguage = (): SupportedLanguage => {
|
||||
return i18n.language as SupportedLanguage;
|
||||
};
|
||||
|
||||
// Helper function to change language
|
||||
export const changeLanguage = async (language: SupportedLanguage): Promise<void> => {
|
||||
await i18n.changeLanguage(language);
|
||||
};
|
||||
|
||||
// Helper function to get language name
|
||||
export const getLanguageName = (code: SupportedLanguage): string => {
|
||||
const lang = SUPPORTED_LANGUAGES.find((l) => l.code === code);
|
||||
return lang?.nativeName || code;
|
||||
};
|
||||
|
||||
// Helper function to check if language is supported
|
||||
export const isLanguageSupported = (code: string): code is SupportedLanguage => {
|
||||
return SUPPORTED_LANGUAGES.some((lang) => lang.code === code);
|
||||
};
|
||||
|
||||
// Helper function to get device locale info
|
||||
export const getDeviceLocaleInfo = () => {
|
||||
return {
|
||||
locale: Localization.locale,
|
||||
locales: Localization.locales,
|
||||
timezone: Localization.timezone,
|
||||
isRTL: Localization.isRTL,
|
||||
region: Localization.region,
|
||||
};
|
||||
};
|
||||
50
apps/context/apps/mobile/utils/markdown.ts
Normal file
50
apps/context/apps/mobile/utils/markdown.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Utility functions for working with markdown content
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts a title from markdown content
|
||||
* First tries to find an H1 heading, then falls back to the first line
|
||||
* Trims the content to a reasonable length for display
|
||||
*
|
||||
* @param content The markdown content
|
||||
* @param maxLength Maximum length for the extracted title
|
||||
* @returns The extracted title or a default title if none found
|
||||
*/
|
||||
export const extractTitleFromMarkdown = (
|
||||
content: string | null | undefined,
|
||||
maxLength: number = 50
|
||||
): string => {
|
||||
if (!content) return 'Unbenanntes Dokument';
|
||||
|
||||
// Try to find an H1 heading (# Title)
|
||||
const h1Match = content.match(/^#\s+(.+)$/m);
|
||||
if (h1Match && h1Match[1]) {
|
||||
return truncateTitle(h1Match[1], maxLength);
|
||||
}
|
||||
|
||||
// Fall back to first line
|
||||
const firstLine = content.split('\n')[0].trim();
|
||||
if (firstLine) {
|
||||
// Remove markdown formatting from the first line
|
||||
const cleanLine = firstLine
|
||||
.replace(/^[#*_~`>]+\s*/, '') // Remove heading markers, list markers, etc.
|
||||
.replace(/[*_~`]+/g, ''); // Remove bold, italic, etc.
|
||||
|
||||
return truncateTitle(cleanLine, maxLength);
|
||||
}
|
||||
|
||||
return 'Unbenanntes Dokument';
|
||||
};
|
||||
|
||||
/**
|
||||
* Truncates a title to a specified maximum length
|
||||
*
|
||||
* @param title The title to truncate
|
||||
* @param maxLength Maximum length for the title
|
||||
* @returns The truncated title
|
||||
*/
|
||||
const truncateTitle = (title: string, maxLength: number): string => {
|
||||
if (title.length <= maxLength) return title;
|
||||
return title.substring(0, maxLength - 3) + '...';
|
||||
};
|
||||
24
apps/context/apps/mobile/utils/markdownProcessor.ts
Normal file
24
apps/context/apps/mobile/utils/markdownProcessor.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
/**
|
||||
* Utility functions for processing markdown syntax
|
||||
*/
|
||||
|
||||
/**
|
||||
* Processes content in markdown
|
||||
*
|
||||
* @param content The markdown content to process
|
||||
* @param isDark Whether dark mode is enabled
|
||||
* @returns Processed markdown content
|
||||
*/
|
||||
export const processAIContentBlocks = (content: string, isDark: boolean): string => {
|
||||
// Wir entfernen alle speziellen Marker, die früher für die Unterscheidung
|
||||
// zwischen Benutzer- und KI-Eingaben verwendet wurden
|
||||
if (!content) return '';
|
||||
|
||||
// Entferne alle Marker
|
||||
let processedContent = content
|
||||
.replace(/\/\/\/ KI Antwort, .*?, .*?\n\n/g, '')
|
||||
.replace(/\/\/\/ Nutzer, .*?, .*?\n\n/g, '')
|
||||
.replace(/\n\n\/\/\//g, '');
|
||||
|
||||
return processedContent;
|
||||
};
|
||||
134
apps/context/apps/mobile/utils/markdownVariants.ts
Normal file
134
apps/context/apps/mobile/utils/markdownVariants.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/**
|
||||
* Hilfsfunktionen für die Verarbeitung von Varianten in Markdown-Texten
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extrahiert Varianten aus einem Markdown-Text
|
||||
* Format: [Option1, Option2, Option3]
|
||||
*/
|
||||
export interface VariantOption {
|
||||
original: string;
|
||||
selected: string;
|
||||
allOptions: string[];
|
||||
position: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert Varianten aus einem Markdown-Text
|
||||
* @param text Der Markdown-Text
|
||||
* @returns Array von gefundenen Varianten
|
||||
*/
|
||||
export const extractVariantsFromMarkdown = (text: string): VariantOption[] => {
|
||||
const variants: VariantOption[] = [];
|
||||
|
||||
// Suche nach Varianten in eckigen Klammern mit Kommas als Trennzeichen
|
||||
// Format: [Option1, Option2, Option3]
|
||||
const variantRegex = /\[(.*?)\]/g;
|
||||
let match;
|
||||
|
||||
while ((match = variantRegex.exec(text)) !== null) {
|
||||
const fullMatch = match[0]; // z.B. "[Heilbronn, München, Hamburg]"
|
||||
const optionsString = match[1]; // z.B. "Heilbronn, München, Hamburg"
|
||||
const options = optionsString.split(',').map((option) => option.trim());
|
||||
|
||||
if (options.length > 0) {
|
||||
variants.push({
|
||||
original: fullMatch,
|
||||
selected: options[0], // Die erste Option ist standardmäßig ausgewählt
|
||||
allOptions: options,
|
||||
position: {
|
||||
start: match.index,
|
||||
end: match.index + fullMatch.length,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return variants;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ersetzt Varianten in einem Text mit den ausgewählten Optionen
|
||||
* @param text Der Originaltext mit Varianten
|
||||
* @param variants Die Varianten mit ausgewählten Optionen
|
||||
* @returns Der Text mit ersetzten Varianten
|
||||
*/
|
||||
export const replaceVariantsInText = (text: string, variants: VariantOption[]): string => {
|
||||
let updatedContent = text;
|
||||
|
||||
// Sortiere die Varianten nach Position (von hinten nach vorne),
|
||||
// damit die Indizes nicht durcheinander kommen, wenn wir Text ersetzen
|
||||
const sortedVariants = [...variants].sort((a, b) => b.position.start - a.position.start);
|
||||
|
||||
// Ersetze alle Varianten mit der ausgewählten Option
|
||||
sortedVariants.forEach(({ original, selected, position }) => {
|
||||
updatedContent =
|
||||
updatedContent.substring(0, position.start) +
|
||||
selected +
|
||||
updatedContent.substring(position.end);
|
||||
});
|
||||
|
||||
return updatedContent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Berechnet die Gesamtzahl der möglichen Kombinationen von Varianten
|
||||
* @param variants Die Varianten
|
||||
* @returns Die Anzahl der möglichen Kombinationen
|
||||
*/
|
||||
export const calculateTotalCombinations = (variants: VariantOption[]): number => {
|
||||
return variants.reduce((total, variant) => total * variant.allOptions.length, 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generiert alle möglichen Kombinationen von Varianten
|
||||
* @param text Der Originaltext mit Varianten
|
||||
* @param variants Die Varianten
|
||||
* @returns Array von Texten mit allen möglichen Kombinationen
|
||||
*/
|
||||
export const generateAllCombinations = (text: string, variants: VariantOption[]): string[] => {
|
||||
if (variants.length === 0) {
|
||||
return [text];
|
||||
}
|
||||
|
||||
// Rekursive Funktion zum Generieren aller Kombinationen
|
||||
const generateCombinations = (
|
||||
currentVariants: VariantOption[],
|
||||
currentIndex: number,
|
||||
currentSelections: string[]
|
||||
): string[] => {
|
||||
// Basisfall: Alle Varianten wurden verarbeitet
|
||||
if (currentIndex >= currentVariants.length) {
|
||||
// Erstelle eine Kopie der Varianten mit den aktuellen Auswahlen
|
||||
const variantsWithSelections = currentVariants.map((variant, idx) => ({
|
||||
...variant,
|
||||
selected: currentSelections[idx],
|
||||
}));
|
||||
|
||||
// Ersetze die Varianten im Text
|
||||
return [replaceVariantsInText(text, variantsWithSelections)];
|
||||
}
|
||||
|
||||
// Rekursiver Fall: Generiere Kombinationen für jede Option der aktuellen Variante
|
||||
const results: string[] = [];
|
||||
const currentVariant = currentVariants[currentIndex];
|
||||
|
||||
for (const option of currentVariant.allOptions) {
|
||||
const newSelections = [...currentSelections];
|
||||
newSelections[currentIndex] = option;
|
||||
|
||||
const combinations = generateCombinations(currentVariants, currentIndex + 1, newSelections);
|
||||
|
||||
results.push(...combinations);
|
||||
}
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
// Starte die rekursive Generierung mit leeren Auswahlen
|
||||
const initialSelections = variants.map(() => '');
|
||||
return generateCombinations(variants, 0, initialSelections);
|
||||
};
|
||||
114
apps/context/apps/mobile/utils/mentionProcessor.ts
Normal file
114
apps/context/apps/mobile/utils/mentionProcessor.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/**
|
||||
* Utility functions for processing document mentions in markdown content
|
||||
*/
|
||||
|
||||
// Regex to match XML tags with attributes
|
||||
export const XML_TAG_REGEX = /<mention[^>]*>(.*?)<\/mention>/g;
|
||||
|
||||
// Regular expression to match document mentions in the format [[Document Title]]
|
||||
export const MENTION_REGEX = /\[\[(.*?)\]\]/g;
|
||||
|
||||
// Standard Markdown-Link-Format: [Titel](ID)
|
||||
export const MARKDOWN_LINK_REGEX = /\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
// Legacy format for backward compatibility
|
||||
export const LEGACY_MENTION_REGEX = /@\[(.*?)\]\((.*?)\)/g;
|
||||
|
||||
/**
|
||||
* Extract all document mentions from markdown content
|
||||
*/
|
||||
export const extractMentions = (content: string): Array<{ title: string; id?: string }> => {
|
||||
const mentions: Array<{ title: string; id?: string }> = [];
|
||||
let match;
|
||||
|
||||
// Neue Format: [[Dokumenttitel]]
|
||||
const regex = new RegExp(MENTION_REGEX);
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
mentions.push({
|
||||
title: match[1],
|
||||
});
|
||||
}
|
||||
|
||||
// Standard Markdown-Link-Format: [Titel](ID)
|
||||
const markdownRegex = new RegExp(MARKDOWN_LINK_REGEX);
|
||||
while ((match = markdownRegex.exec(content)) !== null) {
|
||||
mentions.push({
|
||||
title: match[1],
|
||||
id: match[2],
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy Format: @[Dokumenttitel](dokumentId)
|
||||
const legacyRegex = new RegExp(LEGACY_MENTION_REGEX);
|
||||
while ((match = legacyRegex.exec(content)) !== null) {
|
||||
mentions.push({
|
||||
title: match[1],
|
||||
id: match[2],
|
||||
});
|
||||
}
|
||||
|
||||
return mentions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace document mentions in markdown content with custom components
|
||||
* This is used by the markdown renderer to render mentions as interactive components
|
||||
*/
|
||||
export const processMentionsInMarkdown = (content: string): string => {
|
||||
let processedContent = content;
|
||||
|
||||
// Verarbeite das neue Format [[Dokumenttitel]]
|
||||
processedContent = processedContent.replace(MENTION_REGEX, (match, title) => {
|
||||
// Verwende einen speziellen Markdown-Link mit einem data-Attribut
|
||||
return `<mention documentTitle="${title}">${title}</mention>`;
|
||||
});
|
||||
|
||||
// Verarbeite Standard Markdown-Links [Titel](ID)
|
||||
processedContent = processedContent.replace(MARKDOWN_LINK_REGEX, (match, title, id) => {
|
||||
// Prüfe, ob der Link eine Dokument-ID ist (keine URL)
|
||||
if (!id.startsWith('http') && !id.startsWith('www') && !id.includes('/')) {
|
||||
// Verwende einen speziellen Markdown-Link mit einem data-Attribut
|
||||
return `<mention documentId="${id}" documentTitle="${title}">${title}</mention>`;
|
||||
}
|
||||
|
||||
// Wenn es ein normaler Link ist, nicht verändern
|
||||
return match;
|
||||
});
|
||||
|
||||
// Verarbeite das Legacy-Format @[Dokumenttitel](dokumentId) für Abwärtskompatibilität
|
||||
processedContent = processedContent.replace(LEGACY_MENTION_REGEX, (match, title, id) => {
|
||||
// Verwende einen speziellen Markdown-Link mit einem data-Attribut
|
||||
return `<mention documentId="${id}" documentTitle="${title}">${title}</mention>`;
|
||||
});
|
||||
|
||||
return processedContent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Entfernt XML-Tags aus dem Markdown-Inhalt und ersetzt sie durch den Inhalt des Tags
|
||||
* Dies ist nützlich, wenn die Markdown-Komponente die benutzerdefinierten Tags nicht korrekt verarbeitet
|
||||
*/
|
||||
export const cleanMentionTags = (content: string): string => {
|
||||
if (!content) return '';
|
||||
|
||||
// Entferne alle <mention>-Tags und ersetze sie durch den Inhalt des documentTitle-Attributs
|
||||
let cleanedContent = content;
|
||||
|
||||
// Ersetze alle Vorkommen von <mention>-Tags mit ihrem documentTitle
|
||||
// Dieses Pattern ist sehr spezifisch für die Struktur der Tags
|
||||
cleanedContent = cleanedContent.replace(
|
||||
/<mention[^>]*documentTitle="([^"]*)"[^>]*>.*?<\/mention>/g,
|
||||
(match, title) => {
|
||||
return title;
|
||||
}
|
||||
);
|
||||
|
||||
return cleanedContent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if text contains a potential mention
|
||||
*/
|
||||
export const containsPotentialMention = (text: string): boolean => {
|
||||
return text.includes('[[') || text.includes('@') || (text.includes('[') && text.includes(']('));
|
||||
};
|
||||
23
apps/context/apps/mobile/utils/supabase.ts
Normal file
23
apps/context/apps/mobile/utils/supabase.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
// Prüfen, ob wir in einer Browser-Umgebung sind
|
||||
const isBrowser = typeof window !== 'undefined';
|
||||
|
||||
// Nur AsyncStorage importieren, wenn wir in einer Browser-/React Native-Umgebung sind
|
||||
let AsyncStorage;
|
||||
if (isBrowser) {
|
||||
// Dynamischer Import, um SSR-Probleme zu vermeiden
|
||||
AsyncStorage = require('@react-native-async-storage/async-storage').default;
|
||||
}
|
||||
|
||||
export const supabase = createClient(supabaseUrl || '', supabaseAnonKey || '', {
|
||||
auth: {
|
||||
storage: isBrowser ? AsyncStorage : undefined,
|
||||
autoRefreshToken: true,
|
||||
persistSession: isBrowser,
|
||||
detectSessionInUrl: false,
|
||||
},
|
||||
});
|
||||
118
apps/context/apps/mobile/utils/supabaseTest.ts
Normal file
118
apps/context/apps/mobile/utils/supabaseTest.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { supabase } from './supabase';
|
||||
|
||||
/**
|
||||
* Testet die Verbindung zu Supabase
|
||||
* @returns Ein Promise mit dem Ergebnis des Tests
|
||||
*/
|
||||
export const testSupabaseConnection = async () => {
|
||||
try {
|
||||
// Einfache Abfrage, um zu testen, ob die Verbindung funktioniert
|
||||
// Verwende eine einfache Systemabfrage, die keine RLS-Richtlinien auslöst
|
||||
const { data, error } = await supabase.auth.getSession();
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Fehler bei der Verbindung zu Supabase: ${error.message}`,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Verbindung zu Supabase erfolgreich hergestellt!',
|
||||
data,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Unerwarteter Fehler: ${err.message}`,
|
||||
error: err,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Testet die Authentifizierung mit Supabase
|
||||
* @param email E-Mail-Adresse
|
||||
* @param password Passwort
|
||||
* @returns Ein Promise mit dem Ergebnis des Tests
|
||||
*/
|
||||
export const testSupabaseAuth = async (email: string, password: string) => {
|
||||
try {
|
||||
const { data, error } = await supabase.auth.signInWithPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Fehler bei der Authentifizierung: ${error.message}`,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Authentifizierung erfolgreich!',
|
||||
user: data.user,
|
||||
session: data.session,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Unerwarteter Fehler: ${err.message}`,
|
||||
error: err,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Ruft alle Spaces aus der Datenbank ab
|
||||
* @returns Ein Promise mit dem Ergebnis der Abfrage
|
||||
*/
|
||||
export const fetchAllSpaces = async () => {
|
||||
try {
|
||||
// Verwende eine Abfrage mit einer expliziten Bedingung, um RLS-Probleme zu vermeiden
|
||||
// Hier verwenden wir eine Abfrage, die nur öffentliche Spaces abruft oder Spaces, bei denen der Benutzer Mitglied ist
|
||||
const { data: session } = await supabase.auth.getSession();
|
||||
|
||||
// Wenn der Benutzer angemeldet ist, rufe seine Spaces ab
|
||||
if (session?.session?.user) {
|
||||
const userId = session.session.user.id;
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('spaces')
|
||||
.select('id, name, description, created_at')
|
||||
.or(`created_by.eq.${userId},public.eq.true`);
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Fehler beim Abrufen der Spaces: ${error.message}`,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `${data?.length || 0} Spaces erfolgreich abgerufen`,
|
||||
spaces: data || [],
|
||||
};
|
||||
} else {
|
||||
// Wenn kein Benutzer angemeldet ist, gib eine leere Liste zurück
|
||||
return {
|
||||
success: true,
|
||||
message: 'Keine Spaces gefunden (nicht angemeldet)',
|
||||
spaces: [],
|
||||
};
|
||||
}
|
||||
} catch (err: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: `Unerwarteter Fehler: ${err.message}`,
|
||||
error: err,
|
||||
};
|
||||
}
|
||||
};
|
||||
27
apps/context/apps/mobile/utils/textUtils.ts
Normal file
27
apps/context/apps/mobile/utils/textUtils.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Hilfsfunktionen für Textverarbeitung
|
||||
*/
|
||||
|
||||
/**
|
||||
* Zählt die Anzahl der Wörter in einem Text
|
||||
* @param text Der zu zählende Text
|
||||
* @returns Die Anzahl der Wörter im Text
|
||||
*/
|
||||
export const countWords = (text: string): number => {
|
||||
if (!text) return 0;
|
||||
return text
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((word) => word.length > 0).length;
|
||||
};
|
||||
|
||||
/**
|
||||
* Berechnet die geschätzte Lesezeit in Minuten
|
||||
* @param text Der zu lesende Text
|
||||
* @param wordsPerMinute Wörter pro Minute (Standard: 200)
|
||||
* @returns Die geschätzte Lesezeit in Minuten
|
||||
*/
|
||||
export const calculateReadingTime = (text: string, wordsPerMinute: number = 200): number => {
|
||||
const words = countWords(text);
|
||||
return Math.ceil(words / wordsPerMinute);
|
||||
};
|
||||
313
apps/context/apps/mobile/utils/theme/colors.ts
Normal file
313
apps/context/apps/mobile/utils/theme/colors.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/**
|
||||
* Zentrale Farbdefinitionen für die Anwendung
|
||||
* Diese Datei definiert alle Farben und Themes, die in der Anwendung verwendet werden
|
||||
*/
|
||||
|
||||
// Gemeinsame Farben für alle Themes
|
||||
const commonColors = {
|
||||
// Button-Styles
|
||||
button: {
|
||||
// Varianten
|
||||
primary: {
|
||||
background: {
|
||||
default: { light: '#0ea5e9', dark: '#0284c7' },
|
||||
hover: { light: '#0284c7', dark: '#0369a1' },
|
||||
active: { light: '#0369a1', dark: '#075985' },
|
||||
disabled: { light: '#bae6fd', dark: '#075985' },
|
||||
},
|
||||
text: {
|
||||
default: { light: '#ffffff', dark: '#ffffff' },
|
||||
disabled: { light: '#e0f2fe', dark: '#7dd3fc' },
|
||||
},
|
||||
},
|
||||
secondary: {
|
||||
background: {
|
||||
default: { light: '#e5e7eb', dark: '#374151' },
|
||||
hover: { light: '#d1d5db', dark: '#4b5563' },
|
||||
active: { light: '#9ca3af', dark: '#6b7280' },
|
||||
disabled: { light: '#f3f4f6', dark: '#1f2937' },
|
||||
},
|
||||
text: {
|
||||
default: { light: '#111827', dark: '#f9fafb' },
|
||||
active: { light: '#ffffff', dark: '#ffffff' },
|
||||
disabled: { light: '#6b7280', dark: '#9ca3af' },
|
||||
},
|
||||
},
|
||||
outline: {
|
||||
border: {
|
||||
default: { light: '#d1d5db', dark: '#4b5563' },
|
||||
hover: { light: '#9ca3af', dark: '#6b7280' },
|
||||
active: { light: '#6b7280', dark: '#9ca3af' },
|
||||
},
|
||||
text: {
|
||||
default: { light: '#111827', dark: '#f9fafb' },
|
||||
hover: { light: '#374151', dark: '#e5e7eb' },
|
||||
disabled: { light: '#9ca3af', dark: '#4b5563' },
|
||||
},
|
||||
},
|
||||
},
|
||||
// Graustufen
|
||||
gray: {
|
||||
50: '#f9fafb',
|
||||
100: '#f3f4f6',
|
||||
200: '#e5e7eb',
|
||||
300: '#d1d5db',
|
||||
400: '#9ca3af',
|
||||
500: '#6b7280',
|
||||
600: '#4b5563',
|
||||
700: '#374151',
|
||||
800: '#1f2937',
|
||||
900: '#111827',
|
||||
950: '#030712',
|
||||
},
|
||||
|
||||
// Statusfarben
|
||||
success: {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
950: '#052e16',
|
||||
},
|
||||
|
||||
error: {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
950: '#450a0a',
|
||||
},
|
||||
|
||||
warning: {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24',
|
||||
500: '#f59e0b',
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
800: '#92400e',
|
||||
900: '#78350f',
|
||||
950: '#451a03',
|
||||
},
|
||||
};
|
||||
|
||||
// Theme 1: Blau/Indigo (Standard)
|
||||
const theme1 = {
|
||||
name: 'blue',
|
||||
displayName: 'Blau',
|
||||
// Primärfarben
|
||||
primary: {
|
||||
50: '#f0f9ff',
|
||||
100: '#e0f2fe',
|
||||
200: '#bae6fd',
|
||||
300: '#7dd3fc',
|
||||
400: '#38bdf8',
|
||||
500: '#0ea5e9',
|
||||
600: '#0284c7',
|
||||
700: '#0369a1',
|
||||
800: '#075985',
|
||||
900: '#0c4a6e',
|
||||
950: '#082f49',
|
||||
},
|
||||
|
||||
// Akzentfarben
|
||||
accent: {
|
||||
50: '#eef2ff',
|
||||
100: '#e0e7ff',
|
||||
200: '#c7d2fe',
|
||||
300: '#a5b4fc',
|
||||
400: '#818cf8',
|
||||
500: '#6366f1', // Indigo-500
|
||||
600: '#4f46e5',
|
||||
700: '#4338ca',
|
||||
800: '#3730a3',
|
||||
900: '#312e81',
|
||||
950: '#1e1b4b',
|
||||
},
|
||||
|
||||
// Hintergrundfarben
|
||||
background: {
|
||||
light: '#ffffff',
|
||||
dark: '#121212',
|
||||
},
|
||||
|
||||
// Textfarben
|
||||
text: {
|
||||
light: {
|
||||
primary: '#1f2937', // gray-800
|
||||
secondary: '#4b5563', // gray-600
|
||||
tertiary: '#9ca3af', // gray-400
|
||||
inverse: '#f9fafb', // gray-50
|
||||
},
|
||||
dark: {
|
||||
primary: '#f9fafb', // gray-50
|
||||
secondary: '#e5e7eb', // gray-200
|
||||
tertiary: '#9ca3af', // gray-400
|
||||
inverse: '#1f2937', // gray-800
|
||||
},
|
||||
},
|
||||
|
||||
// Randfarben
|
||||
border: {
|
||||
light: '#e5e7eb', // gray-200
|
||||
dark: '#374151', // gray-700
|
||||
},
|
||||
};
|
||||
|
||||
// Theme 2: Grün/Smaragd
|
||||
const theme2 = {
|
||||
name: 'green',
|
||||
displayName: 'Grün',
|
||||
// Primärfarben
|
||||
primary: {
|
||||
50: '#ecfdf5',
|
||||
100: '#d1fae5',
|
||||
200: '#a7f3d0',
|
||||
300: '#6ee7b7',
|
||||
400: '#34d399',
|
||||
500: '#10b981',
|
||||
600: '#059669',
|
||||
700: '#047857',
|
||||
800: '#065f46',
|
||||
900: '#064e3b',
|
||||
950: '#022c22',
|
||||
},
|
||||
|
||||
// Akzentfarben
|
||||
accent: {
|
||||
50: '#f0fdfa',
|
||||
100: '#ccfbf1',
|
||||
200: '#99f6e4',
|
||||
300: '#5eead4',
|
||||
400: '#2dd4bf',
|
||||
500: '#14b8a6', // Teal-500
|
||||
600: '#0d9488',
|
||||
700: '#0f766e',
|
||||
800: '#115e59',
|
||||
900: '#134e4a',
|
||||
950: '#042f2e',
|
||||
},
|
||||
|
||||
// Hintergrundfarben
|
||||
background: {
|
||||
light: '#ffffff',
|
||||
dark: '#0f1b17',
|
||||
},
|
||||
|
||||
// Textfarben
|
||||
text: {
|
||||
light: {
|
||||
primary: '#064e3b', // green-900
|
||||
secondary: '#065f46', // green-800
|
||||
tertiary: '#047857', // green-700
|
||||
inverse: '#f9fafb', // gray-50
|
||||
},
|
||||
dark: {
|
||||
primary: '#d1fae5', // green-100
|
||||
secondary: '#a7f3d0', // green-200
|
||||
tertiary: '#6ee7b7', // green-300
|
||||
inverse: '#064e3b', // green-900
|
||||
},
|
||||
},
|
||||
|
||||
// Randfarben
|
||||
border: {
|
||||
light: '#d1fae5', // green-100
|
||||
dark: '#065f46', // green-800
|
||||
},
|
||||
};
|
||||
|
||||
// Theme 3: Violett/Fuchsia
|
||||
const theme3 = {
|
||||
name: 'purple',
|
||||
displayName: 'Violett',
|
||||
// Primärfarben
|
||||
primary: {
|
||||
50: '#faf5ff',
|
||||
100: '#f3e8ff',
|
||||
200: '#e9d5ff',
|
||||
300: '#d8b4fe',
|
||||
400: '#c084fc',
|
||||
500: '#a855f7',
|
||||
600: '#9333ea',
|
||||
700: '#7e22ce',
|
||||
800: '#6b21a8',
|
||||
900: '#581c87',
|
||||
950: '#3b0764',
|
||||
},
|
||||
|
||||
// Akzentfarben
|
||||
accent: {
|
||||
50: '#fdf4ff',
|
||||
100: '#fae8ff',
|
||||
200: '#f5d0fe',
|
||||
300: '#f0abfc',
|
||||
400: '#e879f9',
|
||||
500: '#d946ef', // Fuchsia-500
|
||||
600: '#c026d3',
|
||||
700: '#a21caf',
|
||||
800: '#86198f',
|
||||
900: '#701a75',
|
||||
950: '#4a044e',
|
||||
},
|
||||
|
||||
// Hintergrundfarben
|
||||
background: {
|
||||
light: '#ffffff',
|
||||
dark: '#1a1025',
|
||||
},
|
||||
|
||||
// Textfarben
|
||||
text: {
|
||||
light: {
|
||||
primary: '#581c87', // purple-900
|
||||
secondary: '#6b21a8', // purple-800
|
||||
tertiary: '#7e22ce', // purple-700
|
||||
inverse: '#f9fafb', // gray-50
|
||||
},
|
||||
dark: {
|
||||
primary: '#f3e8ff', // purple-100
|
||||
secondary: '#e9d5ff', // purple-200
|
||||
tertiary: '#d8b4fe', // purple-300
|
||||
inverse: '#581c87', // purple-900
|
||||
},
|
||||
},
|
||||
|
||||
// Randfarben
|
||||
border: {
|
||||
light: '#f3e8ff', // purple-100
|
||||
dark: '#6b21a8', // purple-800
|
||||
},
|
||||
};
|
||||
|
||||
// Alle verfügbaren Themes
|
||||
export const themes = {
|
||||
blue: theme1,
|
||||
green: theme2,
|
||||
purple: theme3,
|
||||
};
|
||||
|
||||
// Exportiere die Farben des Standard-Themes (Blau)
|
||||
export const colors = {
|
||||
...commonColors,
|
||||
...theme1,
|
||||
};
|
||||
|
||||
// Typdefinitionen für die Themes
|
||||
export type ThemeNames = keyof typeof themes;
|
||||
export type ThemeColors = typeof theme1;
|
||||
export type ColorTheme = typeof colors;
|
||||
3
apps/context/apps/mobile/utils/theme/index.ts
Normal file
3
apps/context/apps/mobile/utils/theme/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from './colors';
|
||||
export * from './theme';
|
||||
export { default as theme } from './theme';
|
||||
117
apps/context/apps/mobile/utils/theme/theme.ts
Normal file
117
apps/context/apps/mobile/utils/theme/theme.ts
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { colors, ColorTheme, themes, ThemeNames } from './colors';
|
||||
import { useAppTheme, ThemeMode } from '~/components/theme/ThemeProvider';
|
||||
|
||||
/**
|
||||
* Theme-Utility für die Anwendung
|
||||
* Bietet Funktionen und Hooks für die einfache Verwendung des Themes
|
||||
*/
|
||||
|
||||
/**
|
||||
* Hook zum Abrufen des aktuellen Themes
|
||||
* @returns Das aktuelle Theme-Objekt mit Farben und Funktionen
|
||||
*/
|
||||
export function useTheme() {
|
||||
const appTheme = useAppTheme();
|
||||
const currentThemeColors = themes[appTheme.themeName] || themes.blue;
|
||||
|
||||
return {
|
||||
...appTheme,
|
||||
colors: {
|
||||
...colors,
|
||||
...currentThemeColors,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsfunktion zum Abrufen einer Farbe basierend auf dem aktuellen Farbschema
|
||||
* @param lightColor Die Farbe im Light-Modus
|
||||
* @param darkColor Die Farbe im Dark-Modus
|
||||
* @returns Die entsprechende Farbe basierend auf dem aktuellen Farbschema
|
||||
*/
|
||||
export function useColorModeValue(lightColor: string, darkColor: string): string {
|
||||
const { isDark } = useTheme();
|
||||
return isDark ? darkColor : lightColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsfunktion zum Generieren von Tailwind-Klassen basierend auf dem Farbschema
|
||||
* @param lightClasses Die Tailwind-Klassen im Light-Modus
|
||||
* @param darkClasses Die Tailwind-Klassen im Dark-Modus
|
||||
* @returns Die kombinierten Tailwind-Klassen
|
||||
*/
|
||||
export function tw(lightClasses: string, darkClasses: string): string {
|
||||
const { isDark } = useTheme();
|
||||
return isDark ? darkClasses : lightClasses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hilfsfunktion zum Generieren von Tailwind-Klassen mit automatischer Dark-Mode-Unterstützung
|
||||
* Beispiel: twMerge('bg-white dark:bg-gray-900', 'text-gray-800 dark:text-gray-200')
|
||||
* @param classes Die Tailwind-Klassen
|
||||
* @returns Die kombinierten Tailwind-Klassen
|
||||
*/
|
||||
export function twMerge(...classes: string[]): string {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiere Theme-Klassen basierend auf dem aktuellen Theme
|
||||
* @param themeName Der Name des Themes
|
||||
* @returns Ein Objekt mit vordefinierten Tailwind-Klassen für das angegebene Theme
|
||||
*/
|
||||
export function getThemeClasses(themeName: ThemeNames = 'blue') {
|
||||
const theme = themes[themeName] || themes.blue;
|
||||
|
||||
return {
|
||||
// Hintergrundfarben
|
||||
background: 'bg-white dark:bg-gray-900',
|
||||
backgroundSecondary: 'bg-gray-50 dark:bg-gray-800',
|
||||
backgroundTertiary: 'bg-gray-100 dark:bg-gray-700',
|
||||
|
||||
// Textfarben
|
||||
text: 'text-gray-800 dark:text-gray-200',
|
||||
textSecondary: 'text-gray-600 dark:text-gray-400',
|
||||
textTertiary: 'text-gray-500 dark:text-gray-500',
|
||||
textAccent: `text-${theme.name}-600 dark:text-${theme.name}-400`,
|
||||
|
||||
// Randfarben
|
||||
border: 'border-gray-200 dark:border-gray-700',
|
||||
borderAccent: `border-${theme.name}-500 dark:border-${theme.name}-400`,
|
||||
|
||||
// Interaktive Elemente
|
||||
button: `bg-${theme.name}-500 hover:bg-${theme.name}-600 text-white`,
|
||||
buttonSecondary:
|
||||
'bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200',
|
||||
|
||||
// Karten und Container
|
||||
card: 'bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm',
|
||||
|
||||
// Formulare
|
||||
input: `bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 focus:ring-2 focus:ring-${theme.name}-500 focus:border-${theme.name}-500`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook zum Abrufen der Theme-Klassen basierend auf dem aktuellen Theme
|
||||
* @returns Ein Objekt mit vordefinierten Tailwind-Klassen für das aktuelle Theme
|
||||
*/
|
||||
export function useThemeClasses() {
|
||||
const { themeName } = useTheme();
|
||||
return getThemeClasses(themeName);
|
||||
}
|
||||
|
||||
// Vordefinierte Theme-Klassen für häufig verwendete UI-Elemente (für Abwärtskompatibilität)
|
||||
export const themeClasses = getThemeClasses();
|
||||
|
||||
// Export aller Theme-Funktionen und -Konstanten
|
||||
export default {
|
||||
colors,
|
||||
themes,
|
||||
useTheme,
|
||||
useColorModeValue,
|
||||
tw,
|
||||
twMerge,
|
||||
getThemeClasses,
|
||||
useThemeClasses,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue