Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Ticket api integration #115

Merged
merged 4 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,31 @@ import { Home } from '@pages/Home';
import { NotFound } from '@pages/NotFound/NotFound';
import { Projects } from '@pages/Projects/Projects';
import { Registration } from '@pages/Registration';
import { apiClient } from '@utils';
import { useTicketStore, useUserStore } from '@store';
import { apiClient, getUserTicket } from '@utils';
import { Route, Routes } from 'react-router-dom';
import { useUserStore } from './store/useUserStore';

function App() {
const setUser = useUserStore((state) => state.setUser);
const setTicket = useTicketStore((state) => state.setTicket);

useEffect(() => {
apiClient.auth.onAuthStateChange((_event, session) => {
console.log(_event);
if (!session?.user) return;
setUser(session.user);

if (_event == 'INITIAL_SESSION') {
getUserTicket(session.user.id)
.then((ticket) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ I remember we mentioned we would want to persist Ticket entries (without image) as soon as someone logs-in for the first time so that logging in claims your ticket number - was this achieved in a different way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was achieved using a trigger directly on Supabase

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right! Thanks, Luis!

setTicket(ticket);
})
.catch((error) => {
console.log(error);
});
}
});
}, [setUser]);
}, [setUser, setTicket]);

return (
<>
Expand Down
5 changes: 5 additions & 0 deletions src/common/types/models/Tickets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type Ticket = {
id: number;
discordId: string;
image: string | null;
};
1 change: 1 addition & 0 deletions src/common/types/models/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Projects';
export * from './Tickets';
3 changes: 3 additions & 0 deletions src/common/utils/supabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createClient } from '@supabase/supabase-js';

export const supabase = createClient(import.meta.env.VITE_PROJECT_URL, import.meta.env.VITE_API_KEY);
5 changes: 3 additions & 2 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Background } from '@components';
import { RootLayout } from '@layouts';
import { useUserStore } from '@store';
import { useTicketStore, useUserStore } from '@store';
import { CTA, FeatureProjects, Hero, Information, Ticket } from './_sections';
import { Contributors } from './_sections/Contributors';

export const Home = () => {
const user = useUserStore((state) => state.user);
const ticket = useTicketStore((state) => state.ticket);

return (
<RootLayout>
Expand All @@ -15,7 +16,7 @@ export const Home = () => {
<Information />
<CTA className="mt-20 text-center" />
<FeatureProjects />
<Ticket avatar={user?.user_metadata.avatar_url} name={user?.user_metadata.full_name} />
<Ticket avatar={user?.user_metadata.avatar_url} name={user?.user_metadata.full_name} number={ticket?.id} />
<Contributors />
</main>
</RootLayout>
Expand Down
51 changes: 34 additions & 17 deletions src/pages/_sections/Ticket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { FC, RefObject, useRef } from 'react';
import { Variant } from '@common';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Nit: it would be awesome if downloadTicket and shareTwitter didn't depend on React-specifics (refs) but on the DOM elements directily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that I should use getElementById?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const downloadTicket = async (element: HTMLElement, ticketId: string | null, providerId: string | null) => {
  // TODO: Send Generated Image to Supabase
  if (ticketId && providerId) {
    try {
      const img = await toBlob(element);
      const dataUrl = await toPng(elementRef.current);

      if (!img) {
        console.error(); // TODO: Alert
        return;
      }

      const link = document.createElement('a');
      link.download = 'hackafor-ticket.png';
      link.href = dataUrl;
};

---
// Consuming it
if (!elementRef.current) return
downloadTicket(elementRef.current, ...)

This way, the downloadTicket function is no longer React-dependent. But again, it's a super nit, feel free to ignore!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your recommendation, I'll implement this.

import { Button } from '@components';
import { useUserStore } from '@store';
import { uploadTicket } from '@utils';
import { Atropos } from 'atropos/react';
import { toPng } from 'html-to-image';
import { toBlob, toPng } from 'html-to-image';

interface TicketProps {
avatar?: string;
Expand All @@ -20,11 +21,17 @@ const sponsors = [
}
];

const downloadTicket = async (elementRef: RefObject<HTMLElement>) => {
const downloadTicket = async (elementRef: RefObject<HTMLElement>, ticketId: string | null, providerId: string | null) => {
// TODO: Send Generated Image to Supabase
if (elementRef.current) {
if (elementRef.current && ticketId && providerId) {
try {
const dataUrl = await toPng(elementRef.current);

if (!dataUrl) {
console.error(); // TODO: Alert
return;
}

const link = document.createElement('a');
link.download = 'hackafor-ticket.png';
link.href = dataUrl;
Expand All @@ -37,22 +44,32 @@ const downloadTicket = async (elementRef: RefObject<HTMLElement>) => {
}
};

const shareTwitter = async (providerId: string | null) => {
if (!providerId) {
console.log('No providerId', providerId);
return; // TODO: Handle correctly
}
const shareTwitter = async (elementRef: RefObject<HTMLElement>, ticketId: string | null, providerId: string | null) => {
if (elementRef.current && ticketId && providerId) {
try {
const img = await toBlob(elementRef.current);

if (!img) {
console.error(); // TODO: Alert
return;
}

const url = `${import.meta.env.VITE_PROJECT_URL}/api/og?providerId=${providerId}`;
navigator.clipboard.writeText(url); // TODO: Alert so user know it was copied to clipboard
await uploadTicket(providerId, ticketId, img);

const text = encodeURIComponent('Estoy participando en la Hackafor!');
const encodedUrl = encodeURIComponent(url);
const hashtags = encodeURIComponent('Hackafor,Afordin');
const url = `${import.meta.env.VITE_PROJECT_URL}/api/og?ticket=${ticketId}`;
navigator.clipboard.writeText(url); // TODO: Alert so user know it was copied to clipboard

const twitterUrl = `https://twitter.com/intent/tweet?text=${text}&url=${encodedUrl}&hashtags=${hashtags}`;
const text = encodeURIComponent('Estoy participando en la Hackafor!');
const encodedUrl = encodeURIComponent(url);
const hashtags = encodeURIComponent('Hackafor,Afordin');

window.open(twitterUrl, '_blank');
const twitterUrl = `https://twitter.com/intent/tweet?text=${text}&url=${encodedUrl}&hashtags=${hashtags}`;

window.open(twitterUrl, '_blank');
} catch (error) {
console.error('Could not capture image:', error);
}
}
};

export const Ticket: FC<TicketProps> = ({
Expand Down Expand Up @@ -137,7 +154,7 @@ export const Ticket: FC<TicketProps> = ({
<div className="flex justify-center gap-x-5 mt-10">
<Button
onClick={() => {
shareTwitter(user?.id);
shareTwitter(ticketRef, number.toString(), user?.id);
}}
hasBorder
variant={Variant.secondary}
Expand All @@ -149,7 +166,7 @@ export const Ticket: FC<TicketProps> = ({
</Button>
<Button
onClick={() => {
downloadTicket(ticketRef);
downloadTicket(ticketRef, number.toString(), user?.id);
}}
variant={Variant.ghost}
>
Expand Down
1 change: 1 addition & 0 deletions src/store/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './useUserStore';
export * from './useTicketStore';
12 changes: 12 additions & 0 deletions src/store/useTicketStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Ticket } from '@common';
import { create } from 'zustand';

interface TicketStore {
ticket: Ticket | null;
setTicket: (ticket: Ticket) => void;
}

export const useTicketStore = create<TicketStore>((set) => ({
ticket: null,
setTicket: (ticket) => set({ ticket })
}));
1 change: 1 addition & 0 deletions src/utils/controller/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './project.controller';
export * from './ticket.controller';
43 changes: 43 additions & 0 deletions src/utils/controller/ticket.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Ticket } from '@common';
import { apiClient } from '../api';

const bucketStorage = import.meta.env.VITE_STORAGE_BUCKET;

export const getUserTicket = async (providerId: string): Promise<Ticket> => {
const { data: ticketData, error } = await apiClient.from('Ticket').select().eq('discord_id', providerId).single();

if (error) {
throw new Error(error.message);
}

return ticketData as Ticket;
};

export const uploadTicket = async (providerId: string, ticketId: string, img: Blob): Promise<void> => {
const { data, error } = await apiClient.storage.from(bucketStorage).upload(`public/ticket-${ticketId}.png`, img!, {
cacheControl: '3600',
upsert: true
});

if (error && error['statusCode'] != '403') {
throw new Error(error.message);
}

if (!data) {
return;
}

const res = await apiClient.from('Ticket').upsert(
{
discord_id: providerId,
image: data.path
},
{
onConflict: 'discord_id'
}
);

if (res.error) {
throw new Error(res.error.message);
}
};
Loading