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(ui): responsive enhancement #2

Merged
merged 8 commits into from
Mar 7, 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
61 changes: 43 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,61 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
# MyaPoll

## Getting Started
![Vercel Deploy](https://vercel-badge-sawa.vercel.app/?app=myapoll&style=for-the-badge)
![Next JS](https://img.shields.io/badge/Next-black?style=for-the-badge&logo=next.js&logoColor=white)

First, run the development server:
## Introduction

![Alt text](./docs/preview.png)

A polling application designed specifically for YouTube livestream, enabling the collection of real-time audience comments as poll responses.

## Usage

1. Launch the web application and start a poll event within the YouTube livestream.
2. Enter the passphrase for authentication purposes.
3. Provide the URL of the live stream, such as <https://www.youtube.com/watch?v=92VgDXjI4Xg>
4. Specify the number of options. By default, the application allows the audience to update their choices using the latest comments, but you also have the option to disable this feature.
5. Click on the 'Start Poll' button to commence the collection of poll responses. A chart will update in realtime to represent to poll response data.
6. Press the 'Stop' button to manually end the poll.
7. The poll result summary will be displayed.
8. Click on 'Next Poll' to initiate another new poll event.

### Notices

🗝️ Secret Code Protection: only designated individuals can use this application.
✨ This webapp consume Youtube Data API v3, which has daily quota limit (free).

## Development

### Setup env var

create `.env.local` in project root directory, fill in your youtube API token and myaPoll app passphrase.

```txt
YT_DATA_API_TOKEN=
YT_DATA_API_TOKEN_DEV=
PASSPHRASE=
PASSPHRASE_DEV=
```

### Start development server

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!

## Deploy on Vercel

```txt
🚨 Remember to add env var above to Vercel.
```

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
50 changes: 46 additions & 4 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,55 @@ import { Toaster } from '@/components/ui/toaster';
import { TooltipProvider } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import StoreProvider from '@/stores/store-provider';
import Head from 'next/head';

const inter = Inter({ subsets: ['latin'] });

// TODO:SEO stuffs, og image
export const metadata: Metadata = {
title: 'MyaPoll🐼',
description: 'MyaPoll🐼',
title: 'Mya88🐼',
description:
'Youtube live stream polling app powered by Next & official data APIv3',
applicationName: 'MyaPoll',
authors: [{ name: 'No.159 Sawa', url: 'https://sawaych.github.io/' }],
generator: 'Next.js',
keywords: ['youtube', 'mya', 'vtuber', 'hkvtuber'],
referrer: 'origin',
creator: 'No.159 Sawa',
publisher: 'Vercel',
themeColor: [{ media: '(prefers-color-scheme: dark)', color: '#da2777' }],
colorScheme: 'dark',
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
},
robots: 'index, follow',
openGraph: {
type: 'website',
url: 'https://myapoll.vercel.app/',
title: 'MyaPoll🐼',
description:
'Youtube live stream polling app powered by Next & official data APIv3',
siteName: 'Mya88',
images: [
{
url: 'https://myapoll.vercel.app/og',
},
],
},
twitter: {
card: 'summary_large_image',
site: '@site',
creator: '@creator',
images: 'https://myapoll.vercel.app/og',
},
icons: [
{
rel: 'icon',
url: '/greeting.webp',
type: 'image/webp',
},
],
};

export default function RootLayout({
Expand All @@ -23,7 +65,7 @@ export default function RootLayout({
}>) {
return (
<html lang='en' suppressHydrationWarning>
<body className={cn('overflow-x-hidden', inter.className)}>
<body className={cn('overflow-x-hidden bg-[#282a36]', inter.className)}>
<ThemeProvider
attribute='class'
defaultTheme='dark'
Expand Down
2 changes: 1 addition & 1 deletion app/og/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export async function GET() {
tw='opacity-30 absolute'
width='1024'
height='570'
src={'https://myapoll.vercel.app/mya-bg.jpg'}
src={'https://myapoll.vercel.app/mya-bg.png'}
alt='og-image'
/>
<h2 tw='flex flex-col text-3xl sm:text-4xl font-bold tracking-tight text-gray-900 text-left'>
Expand Down
2 changes: 1 addition & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import PollApp from '@/components/poll-app';

export default function Home() {
return (
<main className='z-10 flex min-h-dvh flex-col items-center bg-[#282a36] p-4 text-gray-200'>
<main className='z-10 flex min-h-dvh w-screen flex-col items-center p-4 text-gray-200'>
<AppCreditSection />
<PollApp />
</main>
Expand Down
2 changes: 1 addition & 1 deletion components/app-credit-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Image from 'next/image';
const AppCreditSection = () => {
const currentVersion = useProjectVersion();
return (
<div className='flex select-none self-end'>
<div className='mb-4 flex select-none self-end'>
<div className='flex flex-col items-center justify-center'>
<div className='text-[0.7rem]'>MyaPoll {currentVersion}</div>
<div className='text-[0.6rem]'>Created by No.159</div>
Expand Down
2 changes: 1 addition & 1 deletion components/background.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const DotPattern = ({ children }: PropsWithChildren) => (
const CenterContainer = ({ children }: PropsWithChildren) => (
<div
id='bg-center-container'
className='pointer-events-none fixed flex h-dvh w-dvw select-none items-center justify-center break-all opacity-[0.05] mix-blend-screen transition-all'
className='pointer-events-none fixed flex h-dvh w-dvw select-none items-center justify-center break-all opacity-[0.05] transition-all'
>
{children}
</div>
Expand Down
46 changes: 33 additions & 13 deletions components/livestream-metadata-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,43 @@ import { Card, CardContent } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { usePollAppStore } from '@/stores/store';
import Image from 'next/image';
import { MobileView, BrowserView } from 'react-device-detect';

const LiveStreamMetadataCard = () => {
const { liveMetadata } = usePollAppStore();
return (
<Card className='flex flex-col items-center justify-center'>
<CardContent className='flex flex-col items-center justify-center gap-8'>
<Image
src={liveMetadata?.thumbnail ?? ''}
alt='metadata-yt-thumbnail'
width={320}
height={180}
/>
<Label className='px-10'>
{liveMetadata?.title ?? 'Live stream title not found.'}
</Label>
</CardContent>
</Card>
<>
<BrowserView>
<Card className='flex flex-col items-center justify-center'>
<CardContent className='flex flex-col items-center justify-center gap-8 p-4'>
<Image
src={liveMetadata?.thumbnail ?? ''}
alt='metadata-yt-thumbnail'
width={320}
height={180}
/>
<Label className='px-10'>
{liveMetadata?.title ?? 'Live stream title not found.'}
</Label>
</CardContent>
</Card>
</BrowserView>
<MobileView>
<Card className='flex flex-col items-center justify-center'>
<CardContent className='flex flex-col items-center justify-center gap-2 p-4'>
<Image
src={liveMetadata?.thumbnail ?? ''}
alt='metadata-yt-thumbnail'
width={160}
height={90}
/>
<Label className='px-10'>
{liveMetadata?.title ?? 'Live stream title not found.'}
</Label>
</CardContent>
</Card>
</MobileView>
</>
);
};

Expand Down
2 changes: 2 additions & 0 deletions components/placeholder.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const Placeholder = () => <div className='flex h-16'></div>;
export default Placeholder;
12 changes: 10 additions & 2 deletions components/poll-app-core.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import UrlInput from '@/components/url-input';
import { usePollAppStore } from '@/stores/store';
import PrepareSection from '@/components/prepare-section';
import PollProcessResultSection from '@/components/poll-process-result-section';
import { isMobile } from 'react-device-detect';
import { cn } from '@/lib/utils';

const PollAppCore = () => {
const { toast } = useToast();
Expand Down Expand Up @@ -41,6 +43,7 @@ const PollAppCore = () => {
const vid = vidParser(urlInputValue);
if (vid == null || vid.length === 0) {
toast({
variant: 'destructive',
title: '🚨 Oops...',
description: 'Invalid youtube live url format',
});
Expand Down Expand Up @@ -75,7 +78,7 @@ const PollAppCore = () => {
]);

return (
<div className='flex w-dvw flex-col gap-2 p-20'>
<div className='flex w-[calc(100dvw-5rem)] flex-col gap-2 sm:w-dvw sm:p-20'>
<UrlInput
isLoading={isLoading}
isReady={isReady}
Expand All @@ -86,7 +89,12 @@ const PollAppCore = () => {
/>
{isReady && (
<>
<div className='flex flex-row space-x-2'>
<div
className={cn('flex flex-col', {
'flex-row space-x-2': !isMobile,
'space-y-2': isMobile,
})}
>
<LiveStreamMetadataCard />
<PrepareSection />
</div>
Expand Down
2 changes: 2 additions & 0 deletions components/poll-process-result-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import PollSummarySubCard from '@/components/poll-summary-subcard';
import { useChartConfig } from '@/hooks/use-chart-config';
import { updateChartResultParam } from '@/hooks/use-chart-config';
import { useFetchLiveChat } from '@/hooks/use-fetch-livechat';
import Placeholder from '@/components/placeholder';
import { useCallback, useRef } from 'react';
import {
Chart as ChartJS,
Expand Down Expand Up @@ -105,6 +106,7 @@ const PollProcessResultSection = () => {
</CardContent>
</Card>
)}
<Placeholder />
</>
);
};
Expand Down
3 changes: 2 additions & 1 deletion components/prepare-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const PrepareSection = () => {
Allow audience to update his choice using latest comments
</Label>
</div>
<pre className='pl-6 text-sm text-muted-foreground'>
<pre className='whitespace-pre-wrap pl-6 text-sm text-muted-foreground'>
{'For example:\n'}
{'userA: 2\n'}
{'userB: 1\n'}
Expand All @@ -87,6 +87,7 @@ const PrepareSection = () => {
// simple validation
if (numOfOptions <= 0) {
toast({
variant: 'destructive',
title: '🚨 Oops...',
description: 'Require to fill in valid number of options',
});
Expand Down
3 changes: 2 additions & 1 deletion components/ui/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import { cn } from '@/lib/utils';
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}

/* NOTE: mobile please use text-base otherwise iOS chrome will scale up the whole webapp on input focus */
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
Expand Down
4 changes: 2 additions & 2 deletions components/ui/toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
className={cn(
'fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
'fixed bottom-0 top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]',
className
)}
{...props}
Expand All @@ -23,7 +23,7 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;

const toastVariants = cva(
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full',
'group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-bottom-full data-[state=open]:sm:slide-in-from-bottom-full',
{
variants: {
variant: {
Expand Down
2 changes: 1 addition & 1 deletion components/ui/toaster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function Toaster() {
const { toasts } = useToast();

return (
<ToastProvider>
<ToastProvider duration={2000}>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
Expand Down
Binary file added docs/preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion hooks/use-fetch-livechat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ export const useFetchLiveChat = ({ updateChart }: useFetchLiveChatProps) => {
const d = await fetchLiveChatMessage(chatId, nextToken);
if (!d.success) {
setIsLoading(false);
toast({ title: '🚨 Oops...', description: d.message });
toast({
variant: 'destructive',
title: '🚨 Oops...',
description: d.message,
});
return;
}

Expand Down
Loading