initial implementation of new events api
expo is garbage. why can't i suddenly launch the app in dev mode? why do i have to be forced to dev blindly? because fuck you! stable software is too much to ask for, especially in the js world. fuck this garbage piece of shit software. i fucking despise mobile dev with a burning passion
This commit is contained in:
3 files changed
+17
-322
No files matched your search
@@ -1,295 +0,0 @@
|
||||
import { FontAwesome } from '@expo/vector-icons';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import {
|
||||
Linking,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
Text,
|
||||
Platform,
|
||||
View,
|
||||
Image,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import RenderHtml from 'react-native-render-html';
|
||||
|
||||
import theme from 'src/theme';
|
||||
import type { EventType } from 'src/types/data';
|
||||
import type { EventDetailProps } from 'src/types/navigation';
|
||||
|
||||
moment.locale('nb');
|
||||
|
||||
const CHATEAU_NEUF_ADDRESS = 'Chateau Neuf, Slemdalsveien 15, 0369 Oslo';
|
||||
|
||||
const FORMAT_TIME = 'HH:mm';
|
||||
const FORMAT_MONTH = 'MMM';
|
||||
const FORMAT_DAY = 'DD.';
|
||||
const FORMAT_LOCAL_DAY = 'dddd';
|
||||
const FORMAT_YEAR = 'YYYY';
|
||||
|
||||
const formatRelative = (time: string) => moment(time).fromNow();
|
||||
|
||||
const format = (time: string, formatString = FORMAT_TIME) => moment(time).format(formatString);
|
||||
|
||||
const formatTicketText = (event: EventType) => {
|
||||
/* FIXME: This should be handled by API */
|
||||
let regularPrice = '';
|
||||
let memberPrice = '';
|
||||
if (event.price_regular === '0') {
|
||||
event.price_regular = 'Gratis';
|
||||
}
|
||||
if (event.price_member === '0') {
|
||||
event.price_member = 'Gratis';
|
||||
}
|
||||
if (event.price_regular !== 'Gratis') {
|
||||
regularPrice = ' kr';
|
||||
}
|
||||
if (event.price_member !== 'Gratis') {
|
||||
memberPrice = ' kr';
|
||||
}
|
||||
|
||||
let text = '';
|
||||
if (event.price_regular && event.price_member) {
|
||||
text = `Pris: ${event.price_regular}${regularPrice} / Medlemmer: ${event.price_member}${memberPrice}`;
|
||||
} else if (event.price_regular) {
|
||||
text = `Pris: ${event.price_regular}${regularPrice}`;
|
||||
} else if (event.price_member) {
|
||||
text = `Pris (Medlemmer): ${event.price_member}${memberPrice}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const onFacebookUrlPress = (link: string) => {
|
||||
const fbEventPattern = /https?:\/\/www\.facebook\.com\/events\/(\d+)\/?/i;
|
||||
const matches = fbEventPattern.exec(link);
|
||||
let iosURL = link;
|
||||
if (matches && matches.length) {
|
||||
iosURL = `fb://event/?id=${matches[1]}`;
|
||||
}
|
||||
const url =
|
||||
Platform.select({
|
||||
ios: iosURL,
|
||||
android: link,
|
||||
}) || '';
|
||||
Linking.canOpenURL(url)
|
||||
.then((supported) => {
|
||||
if (!supported) {
|
||||
return Linking.openURL(link);
|
||||
}
|
||||
return Linking.openURL(url);
|
||||
})
|
||||
.catch((err) => console.error('An error occurred', err));
|
||||
};
|
||||
|
||||
const onLocationPress = (venue: string) => {
|
||||
if (venue !== 'Annetsteds') {
|
||||
Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${CHATEAU_NEUF_ADDRESS}`);
|
||||
}
|
||||
};
|
||||
|
||||
const VenueLocation = ({ venue }: Pick<EventType, 'venue'>) =>
|
||||
venue !== 'Annetsteds' ? CHATEAU_NEUF_ADDRESS : 'Ukjent adresse';
|
||||
|
||||
const PriceAndTicket = ({ event }: { event: EventType }) => {
|
||||
const text = formatTicketText(event);
|
||||
|
||||
if (text === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (event.ticket_url) {
|
||||
return (
|
||||
<TouchableOpacity style={styles.metaInner} onPress={() => Linking.openURL(event.ticket_url)}>
|
||||
<View style={styles.metaIcon}>
|
||||
<FontAwesome name="credit-card" style={styles.icons} />
|
||||
</View>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<Text style={styles.metaText}>{text}</Text>
|
||||
<Text style={styles.metaSubtitle}>Kjøp billett</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.metaInner}>
|
||||
<View style={styles.metaIcon}>
|
||||
<FontAwesome name="credit-card" style={styles.icons} />
|
||||
</View>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<Text style={styles.metaText}>{text}</Text>
|
||||
<Text style={styles.metaSubtitle}>Billett i døra</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const FaceButton = ({ event }: { event: EventType }) => {
|
||||
if (!event.facebook_url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={() => onFacebookUrlPress(event.facebook_url)} style={styles.metaInner}>
|
||||
<View style={[styles.metaIcon, { marginTop: 0 }]}>
|
||||
<FontAwesome name="facebook-square" style={styles.icons} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={[styles.metaText, { paddingTop: 2 }]}>På Facebook</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const Year = ({ startTime }: { startTime: string }) => {
|
||||
if (moment().year() === moment(startTime).year()) {
|
||||
return null;
|
||||
}
|
||||
return <Text style={styles.year}>{format(startTime, FORMAT_YEAR)}</Text>;
|
||||
};
|
||||
|
||||
const formatTimeFull = (event: EventType) => {
|
||||
const endTime = event.end_time ? ` - ${format(event.end_time, FORMAT_TIME)}` : '';
|
||||
const startTime = `${format(event.start_time, FORMAT_LOCAL_DAY)} kl. ${format(event.start_time, FORMAT_TIME)}`;
|
||||
return `${startTime}${endTime}`;
|
||||
};
|
||||
|
||||
const EventDetail = ({
|
||||
route: {
|
||||
params: { event },
|
||||
},
|
||||
}: EventDetailProps) => {
|
||||
const { width } = useWindowDimensions();
|
||||
return (
|
||||
<ScrollView>
|
||||
{event.thumbnail.medium_large && <Image source={{ uri: event.thumbnail.medium_large }} style={styles.image} />}
|
||||
<View style={theme.card}>
|
||||
{/* Date and title */}
|
||||
<View style={styles.dateAndTitle}>
|
||||
<View style={styles.dateContainer}>
|
||||
<View style={[theme.card, styles.dateCard]}>
|
||||
<Text style={styles.day}>{format(event.start_time, FORMAT_DAY)}</Text>
|
||||
<Text style={styles.month}>{format(event.start_time, FORMAT_MONTH)}</Text>
|
||||
<Year startTime={event.start_time} />
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title} numberOfLines={2}>
|
||||
{event.title.decoded}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* Time and calendar */}
|
||||
<View style={styles.metaInner}>
|
||||
<View style={styles.metaIcon}>
|
||||
<FontAwesome name="clock-o" style={styles.icons} />
|
||||
</View>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<Text style={styles.metaText}>{formatTimeFull(event)}</Text>
|
||||
<Text style={styles.metaSubtitle}>{formatRelative(event.start_time)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
{/* Location and maps */}
|
||||
<TouchableOpacity style={styles.metaInner} onPress={() => onLocationPress(event.venue)}>
|
||||
<View style={[styles.metaIcon, { paddingLeft: 1 }]}>
|
||||
<FontAwesome name="map-pin" style={styles.icons} />
|
||||
</View>
|
||||
<View style={{ flexDirection: 'column' }}>
|
||||
<Text style={styles.metaText}>{event.venue}</Text>
|
||||
<Text style={styles.metaSubtitle} numberOfLines={1}>
|
||||
<VenueLocation venue={event.venue} />
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
<PriceAndTicket event={event} />
|
||||
<FaceButton event={event} />
|
||||
<RenderHtml contentWidth={width} source={{ html: event.content.rendered }} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventDetail;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dateAndTitle: {
|
||||
flexDirection: 'row',
|
||||
marginBottom: 8,
|
||||
},
|
||||
titleContainer: {
|
||||
paddingLeft: 10,
|
||||
justifyContent: 'center',
|
||||
flex: 4,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: theme.colors.neufBlack,
|
||||
},
|
||||
image: {
|
||||
height: 260,
|
||||
width: null,
|
||||
},
|
||||
time: {
|
||||
fontSize: 14,
|
||||
color: theme.colors.lightText,
|
||||
},
|
||||
metaInner: {
|
||||
flexDirection: 'row',
|
||||
marginVertical: 8,
|
||||
},
|
||||
metaIcon: {
|
||||
width: 16,
|
||||
paddingTop: 3,
|
||||
marginHorizontal: 8,
|
||||
marginVertical: 8,
|
||||
},
|
||||
metaText: {
|
||||
color: theme.colors.neufBlack,
|
||||
},
|
||||
metaSubtitle: {
|
||||
color: theme.colors.neufBlack,
|
||||
},
|
||||
content: {
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: theme.colors.neufGray,
|
||||
marginTop: 8,
|
||||
paddingTop: 10,
|
||||
},
|
||||
dateContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
dateCard: {
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
paddingBottom: 6,
|
||||
width: 62,
|
||||
},
|
||||
day: {
|
||||
paddingTop: 5,
|
||||
fontSize: 20,
|
||||
color: theme.colors.neufBlack,
|
||||
textAlign: 'center',
|
||||
},
|
||||
month: {
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
year: {
|
||||
fontSize: 9,
|
||||
color: theme.colors.neufWhite,
|
||||
backgroundColor: theme.colors.neufOrange,
|
||||
textAlign: 'center',
|
||||
paddingRight: 20,
|
||||
paddingBottom: 3,
|
||||
paddingLeft: 20,
|
||||
paddingTop: 3,
|
||||
},
|
||||
icons: {
|
||||
fontSize: 15,
|
||||
},
|
||||
});
|
||||
@@ -1,7 +1,16 @@
|
||||
import { SimpleLineIcons as Icon } from '@expo/vector-icons';
|
||||
import moment from 'moment';
|
||||
import React from 'react';
|
||||
import { StyleSheet, View, Platform, SectionList, TouchableOpacity, ActivityIndicator, Text } from 'react-native';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Platform,
|
||||
SectionList,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
Text,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { Button } from 'react-native-ui-lib';
|
||||
import 'moment/locale/nb';
|
||||
|
||||
@@ -24,21 +33,21 @@ const renderSectionHeader = ({ section }: { section: { title: string } }) => (
|
||||
const Header = () => <View style={styles.listHeader} />;
|
||||
|
||||
const EventList = () => {
|
||||
const { eventsSectioned, refreshing, handleRefresh, handleLoadMore, showEvent, error, loading } = useEventList();
|
||||
const { eventsSectioned, refreshing, handleRefresh, handleLoadMore, error, loading } = useEventList();
|
||||
|
||||
const renderItem = ({ item }: { item: EventType }) => (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
showEvent(item);
|
||||
return Linking.openURL(items.url);
|
||||
}}
|
||||
style={styles.listItem}
|
||||
>
|
||||
<View style={styles.row}>
|
||||
<View style={{ flex: 5 }}>
|
||||
<Text style={styles.listItemTitle} numberOfLines={1}>
|
||||
{item.title.decoded}
|
||||
{item.title}
|
||||
</Text>
|
||||
<Text style={styles.listItemTime}>{formatTime(item.start_time)}</Text>
|
||||
<Text style={styles.listItemTime}>{formatTime(item.nextOccurence.start)}</Text>
|
||||
</View>
|
||||
<View style={styles.arrow}>
|
||||
<Icon name="arrow-right" style={{ color: theme.colors.neufBlack, marginTop: 3 }} />
|
||||
@@ -56,18 +65,6 @@ const EventList = () => {
|
||||
</View>
|
||||
);
|
||||
|
||||
// TODO: temporary. Remove this once the new website is up
|
||||
// @ts-expect-error message is not a property of type. we don't care because this block of code is temporary
|
||||
if (error && error.message === 'I am a teapot') {
|
||||
return (
|
||||
<View style={theme.card}>
|
||||
<Text style={styles.loadingText}>
|
||||
Vi lager ny nettside! Programmet vil derfor være utilgjengelig inntil alt er på plass!
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View style={theme.card}>
|
||||
|
||||
@@ -59,23 +59,19 @@ const useEventList = () => {
|
||||
};
|
||||
|
||||
const fetchEvents = async () => {
|
||||
const url = `${process.env.EXPO_PUBLIC_EVENT_API_URL}/wp-json/wp/v2/events?page=${page}&future=1`;
|
||||
const url = `${process.env.EXPO_PUBLIC_EVENT_API_URL}?view=compact-app`;
|
||||
setLoading(true);
|
||||
|
||||
let eventData;
|
||||
try {
|
||||
const res = await fetchWithTimeout(url);
|
||||
if (res.status === 418) {
|
||||
// TODO: temporary. Remove once new website is up and running
|
||||
throw new Error('I am a teapot');
|
||||
}
|
||||
setTotalPages(parseInt(res.headers.get('x-wp-totalpages') || '0', 10));
|
||||
eventData = await res.json();
|
||||
eventData = await res.json().events;
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
setTotalPages(Math.floor(eventData.total / 10));
|
||||
setEvents(page === 1 ? eventData : [...events, ...eventData]);
|
||||
setEventsSectioned(page === 1 ? toSectionFormat(eventData) : toSectionFormat([...events, ...eventData]));
|
||||
setError(eventData.error || null);
|
||||
@@ -89,9 +85,6 @@ const useEventList = () => {
|
||||
}, []);
|
||||
|
||||
return {
|
||||
showEvent: (event: EventType) => {
|
||||
navigate('EventStack', { screen: 'EventDetail', params: { event } });
|
||||
},
|
||||
handleRefresh,
|
||||
handleLoadMore,
|
||||
error,
|
||||
|
||||
Reference in new issue
Block a user