Skip to content

Commit

Permalink
Add Stargazers (#878)
Browse files Browse the repository at this point in the history
  • Loading branch information
karniv00l authored Nov 6, 2022
1 parent 9b76f04 commit 87175aa
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 43 deletions.
88 changes: 49 additions & 39 deletions src/@types/pocketbase-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,58 +7,67 @@ export type RecordIdString = string
export type UserIdString = string

export type BaseRecord = {
id: RecordIdString
created: IsoDateString
updated: IsoDateString
collectionId: string
collectionName: string
expand?: { [key: string]: any }
id: RecordIdString
created: IsoDateString
updated: IsoDateString
collectionId: string
collectionName: string
expand?: { [key: string]: any }
}

export enum Collections {
IniFiles = 'iniFiles',
Tunes = 'tunes',
Users = 'users',
IniFiles = 'iniFiles',
Stargazers = 'stargazers',
Tunes = 'tunes',
Users = 'users',
}

export type IniFilesRecord = {
signature: string
file: string
ecosystem: 'speeduino' | 'rusefi'
signature: string
file: string
ecosystem: 'speeduino' | 'rusefi'
}

export type IniFilesResponse = IniFilesRecord & BaseRecord

export type StargazersRecord = {
user: RecordIdString
tune: RecordIdString
}

export type StargazersResponse = StargazersRecord & BaseRecord

export type TunesRecord = {
author: RecordIdString
tuneId: string
signature: string
vehicleName: string
engineMake: string
engineCode: string
displacement: number
cylindersCount: number
aspiration: 'na' | 'turbocharged' | 'supercharged'
compression?: number
fuel?: string
ignition?: string
injectorsSize?: number
year?: number
hp?: number
stockHp?: number
readme: string
textSearch: string
visibility: 'public' | 'unlisted'
tuneFile: string
customIniFile?: string
logFiles?: string[]
toothLogFiles?: string[]
author: RecordIdString
tuneId: string
signature: string
stars?: number
vehicleName: string
engineMake: string
engineCode: string
displacement: number
cylindersCount: number
aspiration: 'na' | 'turbocharged' | 'supercharged'
compression?: number
fuel?: string
ignition?: string
injectorsSize?: number
year?: number
hp?: number
stockHp?: number
readme: string
textSearch: string
visibility: 'public' | 'unlisted'
tuneFile: string
customIniFile?: string
logFiles?: string[]
toothLogFiles?: string[]
}

export type TunesResponse = TunesRecord & BaseRecord

export type UsersRecord = {
avatar?: string
avatar?: string
username: string
email: string
verified: boolean
Expand All @@ -67,7 +76,8 @@ export type UsersRecord = {
export type UsersResponse = UsersRecord & BaseRecord

export type CollectionRecords = {
iniFiles: IniFilesRecord
tunes: TunesRecord
users: UsersRecord
iniFiles: IniFilesRecord
stargazers: StargazersRecord
tunes: TunesRecord
users: UsersRecord
}
96 changes: 96 additions & 0 deletions src/components/StarButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {
useEffect,
useState,
} from 'react';
import {
Badge,
Button,
Space,
Tooltip,
} from 'antd';
import {
StarOutlined,
StarFilled,
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { Colors } from '../utils/colors';
import { TuneDataState } from '../types/state';
import useDb from '../hooks/useDb';
import { useAuth } from '../contexts/AuthContext';
import { Routes } from '../routes';

const StarButton = ({ tuneData }: { tuneData: TuneDataState }) => {
const navigate = useNavigate();
const { currentUserToken } = useAuth();
const { toggleStar, isStarredByMe } = useDb();
const [currentStars, setCurrentStars] = useState(tuneData.stars);
const [isCurrentlyStarred, setIsCurrentlyStarred] = useState(false);
const [isLoading, setIsLoading] = useState(true);

const toggleStarClick = async () => {
if (!currentUserToken) {
navigate(Routes.LOGIN);

return;
}

try {
setIsLoading(true);
const { stars, isStarred } = await toggleStar(currentUserToken, tuneData.id);
setCurrentStars(stars);
setIsCurrentlyStarred(isStarred);
setIsLoading(false);
} catch (error) {
setIsLoading(false);
throw error;
}
};

useEffect(() => {
if (!currentUserToken) {
setIsLoading(false);

return;
}

setIsLoading(true);
isStarredByMe(currentUserToken, tuneData.id).then((isStarred) => {
setIsCurrentlyStarred(isStarred);
setIsLoading(false);
}).catch((error) => {
setIsLoading(false);
throw error;
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUserToken, tuneData.id]);

return (
<div style={{ textAlign: 'right' }}>
<Tooltip
title="You must be signed in to star a tune"
placement="bottom"
trigger={currentUserToken ? 'none' : 'hover'}
>
<Button
icon={isCurrentlyStarred ? <StarFilled style={{ color: Colors.YELLOW }} /> : <StarOutlined />}
onClick={toggleStarClick}
loading={isLoading}
>
<Space style={{ marginLeft: 10 }}>
<div>{isCurrentlyStarred ? 'Starred' : 'Star'}</div>
<Badge
count={currentStars}
style={{ backgroundColor: Colors.TEXT, marginTop: -4 }}
showZero
/>
</Space>
</Button>
</Tooltip>
</div>
);
};

export default StarButton;



9 changes: 7 additions & 2 deletions src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export enum OAuthProviders {

interface AuthValue {
currentUser: UsersResponse | null,
currentUserToken: string | null,
signUp: (email: string, password: string, username: string) => Promise<UsersResponse>,
login: (email: string, password: string) => Promise<UsersResponse>,
refreshUser: () => Promise<UsersResponse | null>,
Expand All @@ -64,9 +65,11 @@ const users = client.collection(Collections.Users);
const AuthProvider = (props: { children: ReactNode }) => {
const { children } = props;
const [currentUser, setCurrentUser] = useState<UsersResponse | null>(null);
const [currentUserToken, setCurrentUserToken] = useState<string | null>(null);

const value = useMemo(() => ({
currentUser,
currentUserToken,
signUp: async (email: string, password: string, username: string) => {
try {
const user = await users.create<UsersResponse>({
Expand Down Expand Up @@ -161,13 +164,15 @@ const AuthProvider = (props: { children: ReactNode }) => {
return Promise.reject(new Error(formatError(error)));
}
},
}), [currentUser]);
}), [currentUser, currentUserToken]);

useEffect(() => {
setCurrentUser(client.authStore.model as UsersResponse | null);
setCurrentUserToken(client.authStore.token);

const storeUnsubscribe = client.authStore.onChange((_token, model) => {
const storeUnsubscribe = client.authStore.onChange((token, model) => {
setCurrentUser(model as UsersResponse | null);
setCurrentUserToken(token);
});

return () => {
Expand Down
53 changes: 52 additions & 1 deletion src/hooks/useDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const tunesCollection = client.collection(Collections.Tunes);

const customEndpoint = `${API_URL}/api/custom`;

const headers = (token: string) => ({
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
});

const useDb = () => {
const updateTune = async (id: string, data: TunesRecordPartial): Promise<void> => {
try {
Expand Down Expand Up @@ -97,7 +102,7 @@ const useDb = () => {

try {
const list = await tunesCollection.getList<TunesResponse>(page, perPage, {
sort: '-updated',
sort: '-stars,-updated',
filter,
expand: 'author',
});
Expand Down Expand Up @@ -161,6 +166,50 @@ const useDb = () => {
}
};

const toggleStar = async (currentUserToken: string, tune: string): Promise<{ stars: number, isStarred: boolean }> => {
const response = await fetch(`${customEndpoint}/stargazers/toggleStar`, {
method: 'POST',
headers: headers(currentUserToken),
body: JSON.stringify({ tune }),
});

if (response.ok) {
const { stars, isStarred } = await response.json();

return Promise.resolve({ stars, isStarred });
}

if (response.status === 404) {
return Promise.resolve({ stars: 0, isStarred: false });
}

Sentry.captureException(response);
databaseGenericError(new Error(response.statusText));

return Promise.reject(response.status);
};

const isStarredByMe = async (currentUserToken: string, tune: string): Promise<boolean> => {
const response = await fetch(`${customEndpoint}/stargazers/starredByMe/${tune}`, {
headers: headers(currentUserToken),
});

if (response.ok) {
const { isStarred } = await response.json();

return Promise.resolve(isStarred);
}

if (response.status === 404) {
return Promise.resolve(false);
}

Sentry.captureException(response);
databaseGenericError(new Error(response.statusText));

return Promise.reject(response.status);
};

return {
updateTune,
createTune,
Expand All @@ -169,6 +218,8 @@ const useDb = () => {
searchTunes,
getUserTunes,
autocomplete,
toggleStar,
isStarredByMe,
};
};

Expand Down
1 change: 0 additions & 1 deletion src/pages/Hub.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ const Hub = () => {
displacement: `${tune.displacement}l`,
aspiration: aspirationMapper[tune.aspiration],
updated: formatTime(tune.updated),
stars: 0,
}));
setDataSource(mapped as any);
} catch (error) {
Expand Down
2 changes: 2 additions & 0 deletions src/pages/Info.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Routes } from '../routes';
import { useAuth } from '../contexts/AuthContext';
import { formatTime } from '../utils/time';
import { UsersResponse } from '../@types/pocketbase-types';
import StarButton from '../components/StarButton';

const { Item } = Form;
const rowProps = { gutter: 10 };
Expand Down Expand Up @@ -66,6 +67,7 @@ const Info = ({ tuneData }: { tuneData: TuneDataState }) => {

return (
<div className="small-container">
<StarButton tuneData={tuneData} />
<Divider>Details</Divider>
<Form>
<Row {...rowProps}>
Expand Down

0 comments on commit 87175aa

Please sign in to comment.