Skip to content

Commit

Permalink
Merge pull request #35 from DimensionDev/feat/poll-ui
Browse files Browse the repository at this point in the history
  • Loading branch information
guanbinrui authored Sep 9, 2024
2 parents d23a323 + b071c2c commit 7cbdd7d
Show file tree
Hide file tree
Showing 14 changed files with 172 additions and 83 deletions.
Binary file added public/bg.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed public/checked.png
Binary file not shown.
Binary file added public/selected.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions src/app/api/frames/images/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { IMAGE_ZOOM_SCALE } from '@/constants';
const imagesWorker = createImagesWorker({
imageOptions: {
sizes: {
'1.91:1': { width: 600 * IMAGE_ZOOM_SCALE, height: 400 * IMAGE_ZOOM_SCALE },
'1:1': { width: 600 * IMAGE_ZOOM_SCALE, height: 400 * IMAGE_ZOOM_SCALE },
'1.91:1': { width: 764 * IMAGE_ZOOM_SCALE, height: 400 * IMAGE_ZOOM_SCALE },
'1:1': { width: 600 * IMAGE_ZOOM_SCALE, height: 600 * IMAGE_ZOOM_SCALE },
},
},
});
Expand Down
6 changes: 4 additions & 2 deletions src/app/polls/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RedirectProfile } from '@/components/RedirectProfile';
import { COMMON_APP_TITLE } from '@/constants';
import { env } from '@/constants/env';
import { IMAGE_QUERY_SCHEMA } from '@/constants/zod';
import { getPoll } from '@/services/getPoll';

interface PageProps {
params: { id: string };
Expand All @@ -17,8 +18,9 @@ export async function generateMetadata({ params, searchParams }: PageProps): Pro
...searchParams,
id: params.id,
});
const ogImage = `${env.external.NEXT_PUBLIC_HOST}/firefly.png`;
const metadata = await fetchMetadata(new URL(urlcat('/api/frames', queryData), env.external.NEXT_PUBLIC_HOST));
const poll = await getPoll(queryData.id, queryData.source, queryData.profileId);
const ogImage = (metadata?.['of:image'] || `${env.external.NEXT_PUBLIC_HOST}/firefly.png`) as string;

if (metadata) {
// update this to support hey.xyz
Expand All @@ -33,7 +35,7 @@ export async function generateMetadata({ params, searchParams }: PageProps): Pro
other: { ...metadata },
openGraph: {
title: COMMON_APP_TITLE,
description: 'Everything app for Web3 natives',
description: poll?.title || 'Everything app for Web3 natives',
images: [ogImage],
},
metadataBase: new URL(env.external.NEXT_PUBLIC_HOST),
Expand Down
207 changes: 146 additions & 61 deletions src/components/PollCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,32 @@ function VoteButton({ text, theme }: VoteButtonProps) {
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: theme.optionTextColor,
height: 40 * IMAGE_ZOOM_SCALE,
color: theme.secondTextColor,
height: 20 * IMAGE_ZOOM_SCALE,
width: '100%',
borderRadius: 10 * IMAGE_ZOOM_SCALE,
fontSize: 16 * IMAGE_ZOOM_SCALE,
fontWeight: 'bold',
backgroundColor: theme.optionBgColor,
fontSize: 18 * IMAGE_ZOOM_SCALE,
}}
>
{text}
<span
style={{
marginRight: 9 * IMAGE_ZOOM_SCALE,
width: 12 * IMAGE_ZOOM_SCALE,
height: 12 * IMAGE_ZOOM_SCALE,
borderRadius: '50%',
backgroundColor: theme.secondTextColor,
}}
/>
<div
style={{
display: 'flex',
maxWidth: '90%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{text}
</div>
</div>
);
}
Expand All @@ -52,57 +67,64 @@ function VoteResult({ choice, theme, isMax }: VoteResultProps) {
style={{
position: 'relative',
display: 'flex',
height: 40 * IMAGE_ZOOM_SCALE,
height: 20 * IMAGE_ZOOM_SCALE,
backgroundColor: theme.optionBgColor,
borderRadius: 4 * IMAGE_ZOOM_SCALE,
fontSize: 12 * IMAGE_ZOOM_SCALE,
color: theme.secondTextColor,
overflow: 'hidden',
}}
>
<div
style={{
display: 'flex',
width: choice.percent ? `${choice.percent}%` : `${10 * IMAGE_ZOOM_SCALE}px`,
position: 'absolute',
left: 0,
top: 0,
display: 'flex',
zIndex: 0,
height: '100%',
borderRadius: 10 * IMAGE_ZOOM_SCALE,
backgroundColor: choice.is_select ? theme.optionSelectedBgColor : theme.optionBgColor,
width: `${choice.percent}%`,
backgroundColor: isMax ? theme.optionSelectedBgColor : theme.optionBgColor,
}}
/>
<div
style={{
position: 'absolute',
left: 0,
top: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
paddingLeft: 20 * IMAGE_ZOOM_SCALE,
height: '100%',
alignItems: 'center',
zIndex: 1,
width: '100%',
fontSize: 16 * IMAGE_ZOOM_SCALE,
fontWeight: 'bold',
color: choice.is_select ? theme.optionSelectedTextColor : theme.optionTextColor,
height: '100%',
paddingLeft: 4 * IMAGE_ZOOM_SCALE,
paddingRight: 8 * IMAGE_ZOOM_SCALE,
}}
>
<span
<div
style={{
display: 'flex',
alignItems: 'center',
gap: 8 * IMAGE_ZOOM_SCALE,
width: '86%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
<span style={{ display: 'flex' }}>{choice.name}</span>
{choice.is_select ? (
// eslint-disable-next-line @next/next/no-img-element
<img
alt={choice.name}
src={`${env.external.NEXT_PUBLIC_HOST}/checked.png`}
alt="selected"
style={{
display: 'flex',
width: 20 * IMAGE_ZOOM_SCALE,
height: 20 * IMAGE_ZOOM_SCALE,
width: 16 * IMAGE_ZOOM_SCALE,
height: 16 * IMAGE_ZOOM_SCALE,
}}
src={`${env.external.NEXT_PUBLIC_HOST}/selected.png`}
/>
) : null}
</span>
<span style={{ display: 'flex', color: isMax ? theme.optionTextColor : theme.percentColor }}>
{choice.percent}%
</span>
{choice.name}
</div>
<span>{choice.percent}%</span>
</div>
</div>
);
Expand All @@ -114,62 +136,125 @@ export function PollCard({ poll, locale, profileId }: PollCardProps) {
const t = createFrameTranslator(locale);

const maxPercent = Math.max(...choice_detail.map((choice) => choice.percent || 0));
const showResult = is_end || choice_detail.some((choice) => choice.is_select);

return (
<div
style={{
display: 'flex',
alignItems: 'flex-end',
justifyContent: 'center',
flexDirection: 'column',
justifyContent: 'space-between',
width: '100%',
height: '100%',
backgroundColor: themeConfig.cardBgColor,
lineHeight: 1.2,
fontSize: 24 * IMAGE_ZOOM_SCALE,
backgroundImage: `url(${env.external.NEXT_PUBLIC_HOST}/bg.jpeg)`,
backgroundSize: '100% 100%',
}}
>
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: 12 * IMAGE_ZOOM_SCALE,
padding: 20 * IMAGE_ZOOM_SCALE,
}}
>
{choice_detail.map((choice, index) =>
(!!profileId && is_end) || choice_detail.some((choice) => choice.is_select) ? (
<VoteResult
key={index}
choice={choice}
theme={themeConfig}
isMax={!!choice.percent && choice.percent === maxPercent}
/>
) : (
<VoteButton key={index} theme={themeConfig} text={choice.name} />
),
)}
<div
style={{
display: 'flex',
marginTop: 120 * IMAGE_ZOOM_SCALE,
justifyContent: 'center',
width: '100%',
fontSize: 12 * IMAGE_ZOOM_SCALE,
color: is_end ? themeConfig.secondTextColor : themeConfig.optionSelectedTextColor,
opacity: is_end ? 0.5 : 1,
}}
>
{t`${vote_count} vote${vote_count !== 1 ? 'S' : ''}`} · {getPollTimeLeft(poll, locale)}
</div>
<div
style={{
display: 'flex',
justifyContent: 'center',
marginTop: (profileId ? 6 : 30) * IMAGE_ZOOM_SCALE,
padding: `0 ${72 * IMAGE_ZOOM_SCALE}px`,
width: '100%',
color: themeConfig.secondTextColor,
fontSize: 16 * IMAGE_ZOOM_SCALE,
textAlign: 'center',
}}
>
<div
style={{
display: '-webkit-box',
WebkitLineClamp: profileId ? 2 : 5,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
wordBreak: 'break-word',
maxWidth: '100%',
}}
>
{poll.title}
</div>
</div>
{profileId ? (
<div
style={{
display: 'flex',
gap: 4 * IMAGE_ZOOM_SCALE,
justifyContent: 'center',
marginTop: 12 * IMAGE_ZOOM_SCALE,
}}
>
{[0, 1, 2].map((index) => (
<span
key={index}
style={{
width: 4 * IMAGE_ZOOM_SCALE,
height: 4 * IMAGE_ZOOM_SCALE,
backgroundColor: themeConfig.secondTextColor,
}}
/>
))}
</div>
) : null}
</div>
{profileId ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
gap: 7 * IMAGE_ZOOM_SCALE,
padding: `0 ${114 * IMAGE_ZOOM_SCALE}px`,
}}
>
{choice_detail.map((choice, index) => {
return showResult ? (
<VoteResult
key={index}
choice={choice}
theme={themeConfig}
isMax={!!choice.percent && choice.percent === maxPercent}
/>
) : (
<VoteButton key={index} text={choice.name} theme={themeConfig} />
);
})}
</div>
) : null}
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginTop: 40 * IMAGE_ZOOM_SCALE,
padding: 20 * IMAGE_ZOOM_SCALE,
width: '100%',
justifyContent: 'center',
paddingBottom: 16 * IMAGE_ZOOM_SCALE,
fontSize: 10 * IMAGE_ZOOM_SCALE,
color: themeConfig.secondTextColor,
fontSize: 12 * IMAGE_ZOOM_SCALE,
}}
>
<span>
{profileId ? (
<span>
{t`${vote_count} vote${vote_count !== 1 ? 's' : ''}`} · {getPollTimeLeft(poll, locale)}
</span>
) : null}
</span>
<span>{t`via Firefly`}</span>
{t`Via Firefly`}
</div>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const MAX_CHARS_POLL_OPTION = 25;

export const PER_USER_VOTE_LIMIT = 1;

export const COMMON_APP_TITLE = 'Firefly';
export const COMMON_APP_TITLE = 'Polls via Firefly';

export const IS_PRODUCTION = env.external.NEXT_PUBLIC_VERCEL_ENV === VERCEL_NEV.Production;
export const IS_DEVELOPMENT = env.external.NEXT_PUBLIC_VERCEL_ENV === VERCEL_NEV.Development;
Expand Down
4 changes: 2 additions & 2 deletions src/constants/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ export enum IMAGE_THEME {

export const DARK_THEME: PollTheme = {
titleColor: '#f5f5f5',
optionBgColor: '#ffffff21',
optionBgColor: 'rgba(172, 157, 246, 0.2)',
optionTextColor: '#f5f5f5',
optionSelectedTextColor: '#AC9DF6',
optionSelectedBgColor: '#ffffff21',
optionSelectedBgColor: '#AC9DF6',
cardBgColor: '#181818',
secondTextColor: '#ffffff',
percentColor: '#FFFFFF70',
Expand Down
6 changes: 3 additions & 3 deletions src/helpers/getPollTimeLeft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ export const getPollTimeLeft = (poll: Poll, locale: LOCALE) => {

const days = Math.floor(timeLeft / (1000 * 60 * 60 * 24));
if (days >= 1) {
return t`${days} day${days > 1 ? 's' : ''} left`;
return t`${days} day${days > 1 ? 'S' : ''} left`;
}

const hours = Math.floor(timeLeft / (1000 * 60 * 60));
if (hours >= 1) {
return t`${hours} hour${hours > 1 ? 's' : ''} left`;
return t`${hours} hour${hours > 1 ? 'S' : ''} left`;
}

const minutes = Math.floor(timeLeft / (1000 * 60));
if (minutes >= 1) {
return t`${minutes} minute${minutes > 1 ? 's' : ''} left`;
return t`${minutes} minute${minutes > 1 ? 'S' : ''} left`;
}

return t`Less than a minute left`;
Expand Down
3 changes: 2 additions & 1 deletion src/helpers/parsePollWithZod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ export const parsePollWithZod = (poll: Poll | null, locale: LOCALE, currentVoteI
.object(
{
poll_id: z.string(),
title: z.string(),
created_time: z.number().int().positive(),
end_time: z.number().int().positive(),
is_end: z.boolean(),
vote_count: z.number().int().min(0),
type: z.nativeEnum(POLL_CHOICE_TYPE),
multiple_count: z.number().int().min(0).optional().default(PER_USER_VOTE_LIMIT),
multiple_count: z.number().int().min(0).default(PER_USER_VOTE_LIMIT).catch(PER_USER_VOTE_LIMIT),
choice_detail: z.array(
z.object({
id: z.number().int().positive(),
Expand Down
Loading

0 comments on commit 7cbdd7d

Please sign in to comment.