feat: hackthons page componets, stories api

This commit is contained in:
MTG2000
2022-05-19 20:46:15 +03:00
parent c30566cf14
commit e29ced05f5
26 changed files with 1094 additions and 48 deletions

View File

@@ -18,6 +18,7 @@ export interface NexusGenInputs {
export interface NexusGenEnums {
POST_TYPE: "Bounty" | "Question" | "Story"
VOTE_ITEM_TYPE: "Bounty" | "Comment" | "Project" | "Question" | "Story" | "User"
}
export interface NexusGenScalars {
@@ -131,6 +132,15 @@ export interface NexusGenObjects {
payment_hash: string; // String!
payment_request: string; // String!
}
Vote2: { // root type
amount_in_sat: number; // Int!
id: number; // Int!
item_id: number; // Int!
item_type: NexusGenEnums['VOTE_ITEM_TYPE']; // VOTE_ITEM_TYPE!
paid: boolean; // Boolean!
payment_hash: string; // String!
payment_request: string; // String!
}
}
export interface NexusGenInterfaces {
@@ -193,6 +203,7 @@ export interface NexusGenFieldTypes {
Mutation: { // field return type
confirmVote: NexusGenRootTypes['Vote']; // Vote!
vote: NexusGenRootTypes['Vote']; // Vote!
vote2: NexusGenRootTypes['Vote2']; // Vote2!
}
PostComment: { // field return type
author: NexusGenRootTypes['User']; // User!
@@ -275,6 +286,15 @@ export interface NexusGenFieldTypes {
payment_request: string; // String!
project: NexusGenRootTypes['Project']; // Project!
}
Vote2: { // field return type
amount_in_sat: number; // Int!
id: number; // Int!
item_id: number; // Int!
item_type: NexusGenEnums['VOTE_ITEM_TYPE']; // VOTE_ITEM_TYPE!
paid: boolean; // Boolean!
payment_hash: string; // String!
payment_request: string; // String!
}
PostBase: { // field return type
author: NexusGenRootTypes['User']; // User!
body: string; // String!
@@ -335,6 +355,7 @@ export interface NexusGenFieldTypeNames {
Mutation: { // field return type name
confirmVote: 'Vote'
vote: 'Vote'
vote2: 'Vote2'
}
PostComment: { // field return type name
author: 'User'
@@ -417,6 +438,15 @@ export interface NexusGenFieldTypeNames {
payment_request: 'String'
project: 'Project'
}
Vote2: { // field return type name
amount_in_sat: 'Int'
id: 'Int'
item_id: 'Int'
item_type: 'VOTE_ITEM_TYPE'
paid: 'Boolean'
payment_hash: 'String'
payment_request: 'String'
}
PostBase: { // field return type name
author: 'User'
body: 'String'
@@ -439,6 +469,11 @@ export interface NexusGenArgTypes {
amount_in_sat: number; // Int!
project_id: number; // Int!
}
vote2: { // args
amount_in_sat: number; // Int!
item_id: number; // Int!
item_type: NexusGenEnums['VOTE_ITEM_TYPE']; // VOTE_ITEM_TYPE!
}
}
Query: {
allProjects: { // args

View File

@@ -54,6 +54,7 @@ type LnurlDetails {
type Mutation {
confirmVote(payment_request: String!, preimage: String!): Vote!
vote(amount_in_sat: Int!, project_id: Int!): Vote!
vote2(amount_in_sat: Int!, item_id: Int!, item_type: VOTE_ITEM_TYPE!): Vote2!
}
enum POST_TYPE {
@@ -155,6 +156,15 @@ type User {
name: String!
}
enum VOTE_ITEM_TYPE {
Bounty
Comment
Project
Question
Story
User
}
type Vote {
amount_in_sat: Int!
id: Int!
@@ -162,4 +172,14 @@ type Vote {
payment_hash: String!
payment_request: String!
project: Project!
}
type Vote2 {
amount_in_sat: Int!
id: Int!
item_id: Int!
item_type: VOTE_ITEM_TYPE!
paid: Boolean!
payment_hash: String!
payment_request: String!
}

View File

@@ -0,0 +1,51 @@
const {
intArg,
objectType,
stringArg,
extendType,
nonNull,
} = require('nexus');
const Hackathon = objectType({
name: 'Hackathon',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('description');
t.nonNull.string('cover_image');
t.nonNull.string('date');
t.nonNull.string('location');
t.nonNull.string('website');
t.nonNull.list.nonNull.field('topics', {
type: "Topic",
resolve: (parent) => {
return []
}
});
}
})
const getAllHackathons = extendType({
type: "Query",
args: {
sortBy: stringArg(),
topic: stringArg(),
},
definition(t) {
t.nonNull.list.nonNull.field('getAllHackathons', {
type: "Hackathon",
resolve(_, args) {
return [];
}
})
}
})
module.exports = {
// Types
Hackathon,
// Queries
getAllHackathons,
}

View File

@@ -10,6 +10,7 @@ const {
arg,
} = require('nexus');
const { paginationArgs } = require('./helpers');
const { prisma } = require('../prisma')
const POST_TYPE = enumType({
@@ -17,6 +18,29 @@ const POST_TYPE = enumType({
members: ['Story', 'Bounty', 'Question'],
})
const Topic = objectType({
name: 'Topic',
definition(t) {
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('icon');
}
})
const allTopics = extendType({
type: "Query",
definition(t) {
t.nonNull.list.nonNull.field('allTopics', {
type: "Topic",
resolve: () => {
return prisma.topic.findMany({
});
}
})
}
})
const PostBase = interfaceType({
name: 'PostBase',
@@ -27,14 +51,14 @@ const PostBase = interfaceType({
t.nonNull.int('id');
t.nonNull.string('title');
t.nonNull.string('date');
t.nonNull.field('author', {
type: "User"
});
t.nonNull.string('excerpt');
t.nonNull.string('body');
t.nonNull.list.nonNull.field('tags', {
type: "Tag"
});
t.nonNull.field('topic', {
type: "Topic"
});
t.nonNull.int('votes_count');
},
})
@@ -49,8 +73,18 @@ const Story = objectType({
t.nonNull.string('cover_image');
t.nonNull.int('comments_count');
t.nonNull.list.nonNull.field('comments', {
type: "PostComment"
})
type: "PostComment",
resolve: (parent) => {
return prisma.story.findUnique({ where: { id: parent.id } }).comments();
}
});
t.nonNull.field('author', {
type: "User",
resolve: (parent) => {
return prisma.story.findUnique({ where: { id: parent.id } }).user();
}
});
},
})
@@ -79,7 +113,13 @@ const Bounty = objectType({
t.nonNull.int('applicants_count');
t.nonNull.list.nonNull.field('applications', {
type: "BountyApplication"
})
});
t.nonNull.field('author', {
type: "User",
resolve: (parent) => {
return prisma.bounty.findUnique({ where: { id: parent.id } }).user();
}
});
},
})
@@ -93,8 +133,18 @@ const Question = objectType({
});
t.nonNull.int('answers_count');
t.nonNull.list.nonNull.field('comments', {
type: "PostComment"
})
type: "PostComment",
resolve: (parent) => {
return prisma.question.findUnique({ where: { id: parent.id } }).comments();
}
});
t.nonNull.field('author', {
type: "User",
resolve: (parent) => {
return prisma.question.findUnique({ where: { id: parent.id } }).user();
}
});
},
})
@@ -130,14 +180,18 @@ const getFeed = extendType({
...paginationArgs({ take: 10 }),
sortBy: stringArg({
default: "all"
}),
category: stringArg({
default: "all"
})
}), // all, popular, trending, newest
topic: intArg()
},
resolve(_, { take, skip }) {
const feed = []
return feed.slice(skip, skip + take);
resolve(_, { take, skip, topic, sortBy, }) {
return prisma.story.findMany({
orderBy: { createdAt: "desc" },
where: {
topic_id: topic,
},
skip,
take,
});
}
})
}
@@ -152,16 +206,22 @@ const getTrendingPosts = extendType({
args: {
},
resolve() {
return [];
const now = new Date();
const lastWeekDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7).toUTCString()
return prisma.story.findMany({
take: 5,
where: {
createdAt: {
gt: lastWeekDate
}
}
})
}
})
}
})
const getPostById = extendType({
type: "Query",
definition(t) {
@@ -208,6 +268,7 @@ const getPostById = extendType({
module.exports = {
// Types
POST_TYPE,
Topic,
PostBase,
BountyApplication,
Bounty,
@@ -216,6 +277,7 @@ module.exports = {
PostComment,
Post,
// Queries
allTopics,
getFeed,
getPostById,
getTrendingPosts

View File

@@ -4,6 +4,8 @@ const {
extendType,
nonNull,
stringArg,
arg,
enumType,
} = require('nexus')
const { parsePaymentRequest } = require('invoices');
const { getPaymetRequestForProject, hexToUint8Array } = require('./helpers');
@@ -11,6 +13,11 @@ const { createHash } = require('crypto');
const { prisma } = require('../prisma')
// the types of items we can vote to
const VOTE_ITEM_TYPE = enumType({
name: 'VOTE_ITEM_TYPE',
members: ['Story', 'Bounty', 'Question', 'Project', 'User', 'Comment'],
})
const Vote = objectType({
name: 'Vote',
@@ -21,8 +28,6 @@ const Vote = objectType({
t.nonNull.string('payment_hash');
t.nonNull.boolean('paid');
t.nonNull.field('project', {
type: "Project",
resolve: (parent, args,) => {
@@ -34,6 +39,23 @@ const Vote = objectType({
}
})
const Vote2 = objectType({
name: 'Vote2',
definition(t) {
t.nonNull.int('id');
t.nonNull.int('amount_in_sat');
t.nonNull.string('payment_request');
t.nonNull.string('payment_hash');
t.nonNull.boolean('paid');
t.nonNull.field('item_type', {
type: "VOTE_ITEM_TYPE"
})
t.nonNull.int('item_id');
}
})
const LnurlDetails = objectType({
name: 'LnurlDetails',
@@ -45,6 +67,7 @@ const LnurlDetails = objectType({
}
})
// This is the old voting mutation, it can only vote for projects (SHOULD BE REPLACED BY THE NEW VOTE MUTATION WHEN THAT ONE IS WORKING)
const voteMutation = extendType({
type: "Mutation",
definition(t) {
@@ -77,6 +100,42 @@ const voteMutation = extendType({
})
// This is the new voting mutation, it can vote for any type of item that we define in the VOTE_ITEM_TYPE enum
const vote2Mutation = extendType({
type: "Mutation",
definition(t) {
t.nonNull.field('vote2', {
type: "Vote2",
args: {
item_type: arg({
type: nonNull("VOTE_ITEM_TYPE")
}),
item_id: nonNull(intArg()),
amount_in_sat: nonNull(intArg())
},
resolve: async (_, args) => {
const { item_id, item_type, amount_in_sat } = args;
// Create the invoice here according to it's type & get a payment request and a payment hash
return {
id: 111,
amount_in_sat: amount_in_sat,
payment_request: '{{payment_request}}',
payment_hash: '{{payment_hash}}',
paid: true,
item_type: item_type,
item_id: item_id,
}
}
})
}
})
const confirmVoteMutation = extendType({
type: "Mutation",
definition(t) {
@@ -130,11 +189,16 @@ const confirmVoteMutation = extendType({
})
module.exports = {
// Enums
VOTE_ITEM_TYPE,
// Types
Vote,
Vote2,
LnurlDetails,
// Mutations
voteMutation,
vote2Mutation,
confirmVoteMutation
}

View File

@@ -0,0 +1,159 @@
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"username" TEXT NOT NULL,
"lightning_address" TEXT,
"avatar" TEXT NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Story" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"body" TEXT NOT NULL,
"thumbnail_image" TEXT NOT NULL,
"cover_image" TEXT NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
"topic_id" INTEGER NOT NULL,
"user_id" INTEGER,
CONSTRAINT "Story_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Question" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"body" TEXT NOT NULL,
"thumbnail_image" TEXT NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
"topic_id" INTEGER NOT NULL,
"user_id" INTEGER,
CONSTRAINT "Question_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Topic" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"icon" TEXT NOT NULL,
CONSTRAINT "Topic_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "PostComment" (
"id" SERIAL NOT NULL,
"body" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"votes_count" INTEGER NOT NULL DEFAULT 0,
"parent_comment_id" INTEGER,
"user_id" INTEGER,
"story_id" INTEGER,
"question_id" INTEGER,
CONSTRAINT "PostComment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Hackathon" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"date" TEXT NOT NULL,
"cover_image" TEXT NOT NULL,
"description" TEXT NOT NULL,
"location" TEXT NOT NULL,
"website" TEXT NOT NULL,
"votes_count" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "Hackathon_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_StoryToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "_QuestionToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateTable
CREATE TABLE "_HackathonToTopic" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
-- CreateIndex
CREATE UNIQUE INDEX "Topic_title_key" ON "Topic"("title");
-- CreateIndex
CREATE UNIQUE INDEX "_StoryToTag_AB_unique" ON "_StoryToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_StoryToTag_B_index" ON "_StoryToTag"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_QuestionToTag_AB_unique" ON "_QuestionToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_QuestionToTag_B_index" ON "_QuestionToTag"("B");
-- CreateIndex
CREATE UNIQUE INDEX "_HackathonToTopic_AB_unique" ON "_HackathonToTopic"("A", "B");
-- CreateIndex
CREATE INDEX "_HackathonToTopic_B_index" ON "_HackathonToTopic"("B");
-- AddForeignKey
ALTER TABLE "Story" ADD CONSTRAINT "Story_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Story" ADD CONSTRAINT "Story_topic_id_fkey" FOREIGN KEY ("topic_id") REFERENCES "Topic"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Question" ADD CONSTRAINT "Question_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Question" ADD CONSTRAINT "Question_topic_id_fkey" FOREIGN KEY ("topic_id") REFERENCES "Topic"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_story_id_fkey" FOREIGN KEY ("story_id") REFERENCES "Story"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_question_id_fkey" FOREIGN KEY ("question_id") REFERENCES "Question"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "PostComment" ADD CONSTRAINT "PostComment_parent_comment_id_fkey" FOREIGN KEY ("parent_comment_id") REFERENCES "PostComment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_StoryToTag" ADD FOREIGN KEY ("A") REFERENCES "Story"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_StoryToTag" ADD FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_QuestionToTag" ADD FOREIGN KEY ("A") REFERENCES "Question"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_QuestionToTag" ADD FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_HackathonToTopic" ADD FOREIGN KEY ("A") REFERENCES "Hackathon"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_HackathonToTopic" ADD FOREIGN KEY ("B") REFERENCES "Topic"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,23 @@
/*
Warnings:
- You are about to drop the column `created_at` on the `PostComment` table. All the data in the column will be lost.
- You are about to drop the column `date` on the `Question` table. All the data in the column will be lost.
- You are about to drop the column `date` on the `Story` table. All the data in the column will be lost.
- Added the required column `updatedAt` to the `Question` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `Story` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "PostComment" DROP COLUMN "created_at",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- AlterTable
ALTER TABLE "Question" DROP COLUMN "date",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;
-- AlterTable
ALTER TABLE "Story" DROP COLUMN "date",
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;

View File

@@ -7,6 +7,49 @@ generator client {
provider = "prisma-client-js"
}
// -----------------
// Shared
// -----------------
model Tag {
id Int @id @default(autoincrement())
title String @unique
project Project[]
stories Story[]
questions Question[]
}
model Vote {
id Int @id @default(autoincrement())
project Project @relation(fields: [project_id], references: [id])
project_id Int
amount_in_sat Int
payment_request String?
payment_hash String?
preimage String?
paid Boolean @default(false)
}
// -----------------
// Users
// -----------------
model User {
id Int @id @default(autoincrement())
username String @unique
lightning_address String?
avatar String
stories Story[]
questions Question[]
posts_comments PostComment[]
}
// -----------------
// Projects
// -----------------
model Category {
id Int @id @default(autoincrement())
title String
@@ -16,16 +59,6 @@ model Category {
project Project[]
}
model Award {
id Int @id @default(autoincrement())
title String
image String
url String
project Project @relation(fields: [project_id], references: [id])
project_id Int
}
model Project {
id Int @id @default(autoincrement())
title String
@@ -47,20 +80,105 @@ model Project {
tags Tag[]
}
model Vote {
id Int @id @default(autoincrement())
project Project @relation(fields: [project_id], references: [id])
project_id Int
amount_in_sat Int
payment_request String?
payment_hash String?
preimage String?
paid Boolean @default(false)
model Award {
id Int @id @default(autoincrement())
title String
image String
url String
project Project @relation(fields: [project_id], references: [id])
project_id Int
}
model Tag {
// -----------------
// Posts
// -----------------
model Story {
id Int @id @default(autoincrement())
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
body String
thumbnail_image String
cover_image String
votes_count Int @default(0)
topic Topic @relation(fields: [topic_id], references: [id])
topic_id Int
tags Tag[]
user User? @relation(fields: [user_id], references: [id])
user_id Int?
comments PostComment[] @relation("StoryComment")
}
model Question {
id Int @id @default(autoincrement())
title String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
body String
thumbnail_image String
votes_count Int @default(0)
topic Topic @relation(fields: [topic_id], references: [id])
topic_id Int
tags Tag[]
user User? @relation(fields: [user_id], references: [id])
user_id Int?
comments PostComment[] @relation("QuestionComment")
}
model Topic {
id Int @id @default(autoincrement())
title String @unique
icon String
project Project[]
stories Story[]
questions Question[]
hackathons Hackathon[]
}
model PostComment {
id Int @id @default(autoincrement())
body String
createdAt DateTime @default(now())
votes_count Int @default(0)
replies PostComment[] @relation("PostComment_Replies")
parent_comment_id Int?
parent_comment PostComment? @relation("PostComment_Replies", fields: [parent_comment_id], references: [id])
user User? @relation(fields: [user_id], references: [id])
user_id Int?
story Story? @relation("StoryComment", fields: [story_id], references: [id])
story_id Int?
question Question? @relation("QuestionComment", fields: [question_id], references: [id])
question_id Int?
}
// -----------------
// Hackathons
// -----------------
model Hackathon {
id Int @id @default(autoincrement())
title String
date String
cover_image String
description String
location String
website String
votes_count Int @default(0)
topics Topic[]
}

View File

@@ -21,11 +21,11 @@ interface Props {
const btnStylesFill: UnionToObjectKeys<Props, 'color'> = {
none: "",
primary: "bg-primary-500 border-0 hover:bg-primary-400 active:bg-primary-600 text-white",
primary: "bg-primary-500 hover:bg-primary-400 active:bg-primary-600 text-white",
gray: 'bg-gray-100 hover:bg-gray-200 text-gray-900 active:bg-gray-300',
white: 'text-gray-900 bg-gray-25 hover:bg-gray-50',
white: 'border border-gray-300 text-gray-900 bg-gray-25 hover:bg-gray-50',
black: 'text-white bg-black hover:bg-gray-900',
red: "bg-red-600 border-0 hover:bg-red-500 active:bg-red-700 text-white",
red: "bg-red-600 hover:bg-red-500 active:bg-red-700 text-white",
}
const btnStylesOutline: UnionToObjectKeys<Props, 'color'> = {
@@ -38,7 +38,7 @@ const btnStylesOutline: UnionToObjectKeys<Props, 'color'> = {
}
const baseBtnStyles: UnionToObjectKeys<Props, 'variant'> = {
fill: " shadow-sm active:scale-95",
fill: "active:scale-95",
outline: "bg-gray-900 bg-opacity-0 hover:bg-opacity-5 active:bg-opacity-10 border border-gray-200 active:scale-95 "
}
@@ -72,7 +72,7 @@ export default function Button({ color = 'white',
...props }: Props) {
let classes = `
inline-block font-sans rounded-lg font-regular border border-gray-300 hover:cursor-pointer text-center
inline-block font-sans rounded-lg font-regular hover:cursor-pointer text-center
${baseBtnStyles[variant]}
${btnPadding[size]}
${variant === 'fill' ? btnStylesFill[color] : btnStylesOutline[color]}

View File

@@ -63,14 +63,19 @@ export default function Navbar() {
useEffect(() => {
const nav = document.querySelector("nav");
let oldPadding = '';
if (nav) {
const navStyles = getComputedStyle(nav);
if (navStyles.display !== "none") {
dispatch(setNavHeight(nav.clientHeight));
oldPadding = document.body.style.paddingTop
document.body.style.paddingTop = `${nav.clientHeight}px`;
}
}
return () => {
document.body.style.paddingTop = oldPadding
}
}, [dispatch, isMobileScreen, isLargeScreen])

View File

@@ -0,0 +1,38 @@
import { Hackathon } from "src/features/Hackathons/types"
import { IoLocationOutline } from 'react-icons/io5'
import Button from "src/Components/Button/Button"
import Skeleton from "react-loading-skeleton"
export default function HackathonCardSkeleton() {
return (
<div className="rounded-16 bg-white overflow-hidden">
<div className="w-full h-[120px] bg-gray-200" />
<div className="p-16">
<div className="flex flex-col gap-8">
<h3 className="text-body1 font-bold text-gray-900">
<Skeleton width={'100%'} />
</h3>
<p className="text-body3 font-medium text-gray-900">
<Skeleton width={'100%'} />
</p>
<p className="text-body4 font-medium text-gray-600">
<Skeleton width={'50%'} />
</p>
<p className="text-body4 text-gray-600">
<Skeleton width={'100%'} />
<Skeleton width={'40%'} />
</p>
</div>
<div className="mt-16 flex flex-wrap gap-8">
<div className="p-8 bg-gray-50 rounded-8 w-[92px] h-36">
</div>
<div className="p-8 bg-gray-50 rounded-8 w-[92px] h-36">
</div>
</div>
<div className="bg-gray-100 h-[56px] mt-16 rounded-lg">
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { MOCK_DATA } from 'src/mocks/data';
import HackathonCard from './HackathonCard';
import HackathonCardSkeleton from './HackathonCard.Skeleton';
export default {
title: 'Hackathons/Components/Hackathon Card',
component: HackathonCard,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof HackathonCard>;
const Template: ComponentStory<typeof HackathonCard> = (args) => <div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,326px),1fr))]"><HackathonCard {...args} ></HackathonCard></div>
export const Default = Template.bind({});
Default.args = {
hackathon: MOCK_DATA['hackathons'][0]
}
const LoadingTemplate: ComponentStory<typeof HackathonCard> = (args) => <div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,326px),1fr))]"><HackathonCardSkeleton></HackathonCardSkeleton></div>
export const Loading = LoadingTemplate.bind({});
Loading.args = {
}

View File

@@ -0,0 +1,40 @@
import { Hackathon } from "src/features/Hackathons/types"
import { IoLocationOutline } from 'react-icons/io5'
import Button from "src/Components/Button/Button"
export type HackathonCardType = Hackathon;
interface Props {
hackathon: HackathonCardType
}
export default function HackathonCard({ hackathon }: Props) {
return (
<div className="rounded-16 bg-white overflow-hidden">
<img className="w-full h-[120px] object-cover" src={hackathon.cover_image} alt="" />
<div className="p-16">
<div className="flex flex-col gap-8">
<h3 className="text-body1 font-bold text-gray-900">
{hackathon.title}
</h3>
<p className="text-body3 font-medium text-gray-900">
{hackathon.date}
</p>
<p className="text-body4 font-medium text-gray-600">
<IoLocationOutline className="mr-8" /> {hackathon.location}
</p>
<p className="text-body4 text-gray-600">
{hackathon.description}
</p>
</div>
<div className="mt-16 flex flex-wrap gap-8">
{hackathon.topics.map(topic => <div key={topic.id} className="p-8 bg-gray-50 rounded-8 text-body5">{topic.title}</div>)}
</div>
<Button href={hackathon.url} newTab color="gray" fullWidth className="mt-16">
Learn more
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,22 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { MOCK_DATA } from 'src/mocks/data';
import HackathonsList from './HackathonsList';
export default {
title: 'Hackathons/Components/HackathonsList',
component: HackathonsList,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof HackathonsList>;
const Template: ComponentStory<typeof HackathonsList> = (args) => <HackathonsList {...args} ></HackathonsList>
export const Default = Template.bind({});
Default.args = {
items: MOCK_DATA['hackathons']
}

View File

@@ -0,0 +1,32 @@
import { useReachedBottom } from "src/utils/hooks/useReachedBottom"
import { ListComponentProps } from "src/utils/interfaces"
import HackathonCard, { HackathonCardType } from "../HackathonCard/HackathonCard"
import HackathonCardSkeleton from "../HackathonCard/HackathonCard.Skeleton"
type Props = ListComponentProps<HackathonCardType>
export default function HackathonsList(props: Props) {
const { ref } = useReachedBottom<HTMLDivElement>(props.onReachedBottom)
if (props.isLoading)
return <div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,326px),1fr))] gap-24">
{<>
<HackathonCardSkeleton />
<HackathonCardSkeleton />
<HackathonCardSkeleton />
</>
}
</div>
return (
<div ref={ref} className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,326px),1fr))] gap-24">
{
props.items?.map(hackathon => <HackathonCard key={hackathon.id} hackathon={hackathon} />)
}
{props.isFetching && <HackathonCardSkeleton />}
</div>
)
}

View File

@@ -0,0 +1,20 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import SortBy from './SortByFilter';
export default {
title: 'Hackathons/Components/Filters/Sort By',
component: SortBy,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof SortBy>;
const Template: ComponentStory<typeof SortBy> = (args) => <div className="max-w-[326px]"><SortBy {...args as any} ></SortBy></div>
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,45 @@
import React, { useState } from 'react'
const filters = [
{
text: "Upcoming",
value: 'Upcoming'
}, {
text: "Live",
value: 'live'
}, {
text: "Complete",
value: 'complete'
},
]
interface Props {
filterChanged?: (newFilter: string) => void
}
export default function SortByFilter({ filterChanged }: Props) {
const [selected, setSelected] = useState(filters[0].value);
const filterClicked = (newValue: string) => {
if (selected === newValue)
return
setSelected(newValue);
filterChanged?.(newValue);
}
return (
<div className='bg-white border rounded-12 p-16'>
<p className="text-body2 font-bolder text-black mb-16">Sort By</p>
<ul>
{filters.map((f, idx) => <li
key={f.value}
className={`p-12 rounded-8 cursor-pointer font-bold ${f.value === selected && 'bg-gray-100'}`}
onClick={() => filterClicked(f.value)}
>
{f.text}
</li>)}
</ul>
</div>
)
}

View File

@@ -0,0 +1,20 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import TopicsFilter from './TopicsFilter';
export default {
title: 'Hackathons/Components/Filters/Topics',
component: TopicsFilter,
argTypes: {
backgroundColor: { control: 'color' },
},
} as ComponentMeta<typeof TopicsFilter>;
const Template: ComponentStory<typeof TopicsFilter> = (args) => <div className="max-w-[326px]"><TopicsFilter {...args as any} ></TopicsFilter></div>
export const Default = Template.bind({});
Default.args = {
}

View File

@@ -0,0 +1,58 @@
import { useState } from 'react'
const filters = [
{
text: 'Design',
value: 'Design',
icon: "🎨"
},
{
text: 'Development',
value: 'Development',
icon: "💻"
},
{
text: 'Startups',
value: 'Startups',
icon: "🚀"
},
{
text: 'Lightning Network',
value: 'Lightning Network',
icon: "⚡️"
},
]
interface Props {
filterChanged?: (newFilter: string) => void
}
export default function TopicsFilter({ filterChanged }: Props) {
const [selected, setSelected] = useState(filters[0].value);
const filterClicked = (newValue: string) => {
if (selected === newValue)
return
setSelected(newValue);
filterChanged?.(newValue);
}
return (
<div className='bg-white border rounded-12 p-16'>
<p className="text-body2 font-bolder text-black mb-16">Topics</p>
<ul className=' flex flex-col gap-16'>
{filters.map((f, idx) => <li
key={f.value}
className={`flex items-start rounded-8 cursor-pointer font-bold ${f.value === selected && 'bg-gray-50'}`}
onClick={() => filterClicked(f.value)}
>
<span className='bg-gray-50 rounded-8 p-8'>{f.icon}</span>
<span className="self-center px-16">
{f.text}
</span>
</li>)}
</ul>
</div>
)
}

View File

@@ -0,0 +1,51 @@
import { useReducer, useState } from 'react'
import { useFeedQuery } from 'src/graphql'
import { useAppSelector, useInfiniteQuery } from 'src/utils/hooks'
import SortByFilter from '../../Components/SortByFilter/SortByFilter'
import TopicsFilter from '../../Components/TopicsFilter/TopicsFilter'
import styles from './styles.module.scss'
export default function HackathonsPage() {
const [sortByFilter, setSortByFilter] = useState('all')
const [topicsFilter, setTopicsFilter] = useState('all')
const feedQuery = useFeedQuery({
variables: {
take: 10,
skip: 0,
sortBy: sortByFilter,
category: topicsFilter
},
})
const { fetchMore, isFetchingMore } = useInfiniteQuery(feedQuery, 'getFeed')
const { navHeight } = useAppSelector((state) => ({
navHeight: state.ui.navHeight
}));
return (
<div
className={`page-container pt-16 w-full ${styles.grid}`}
>
<aside className='no-scrollbar'>
<div className="sticky"
style={{
top: `${navHeight + 16}px`,
maxHeight: `calc(100vh - ${navHeight}px - 16px)`,
overflowY: "scroll",
}}>
<SortByFilter
filterChanged={setSortByFilter}
/>
<TopicsFilter
filterChanged={setTopicsFilter}
/>
</div>
</aside>
</div>
)
}

View File

@@ -0,0 +1,17 @@
.grid {
display: grid;
grid-template-columns: 0 1fr 0;
gap: 0;
@media screen and (min-width: 680px) {
grid-template-columns: 1fr 2fr 0;
gap: 32px;
}
@media screen and (min-width: 1024px) {
grid-template-columns:
minmax(200px, 1fr)
minmax(50%, 70ch)
minmax(200px, 1fr);
}
}

View File

@@ -0,0 +1,13 @@
export interface Hackathon {
id: number
title: string
date: string
location: string
description: string
cover_image: string
topics: Array<{
id: number,
title: string
}>,
url: string
}

View File

@@ -0,0 +1 @@
export * from './hackathons.interface'

View File

@@ -1,3 +1,4 @@
import { hackathons } from "./data/hackathon";
import { posts, feed, generatePostComments } from "./data/posts";
import { categories, projects } from "./data/projects";
@@ -6,5 +7,6 @@ export const MOCK_DATA = {
categories,
posts,
feed,
hackathons,
generatePostComments: generatePostComments
}

View File

@@ -0,0 +1,94 @@
import { random, randomItem, randomItems } from "src/utils/helperFunctions"
import { getCoverImage } from "./utils"
const topics = [
{
id: 1,
title: '🎨 Design'
},
{
id: 2,
title: '💻 Hardware'
},
{
id: 3,
title: '⚡️ Lightning'
},
{
id: 4,
title: '🚀 Startups'
},
{
id: 5,
title: '💸 Bitcoin'
},
]
const generateTopics = () => randomItems(
Math.floor(random(1, 4)),
...topics
)
export const hackathons = [
{
id: 1,
title: 'Fulmo Hackday',
date: '22nd - 28th March, 2022',
location: "Instanbul, Turkey",
cover_image: getCoverImage(),
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.",
topics: generateTopics(),
url: "https://bolt.fun/hackathons/shock-the-web"
},
{
id: 2,
title: 'Lightning Leagues',
date: '22nd - 28th March, 2022',
location: "Instanbul, Turkey",
cover_image: getCoverImage(),
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.",
topics: generateTopics(),
url: "https://bolt.fun/hackathons/shock-the-web"
},
{
id: 3,
title: 'Surfing on Lightning',
date: '22nd - 28th March, 2022',
location: "Instanbul, Turkey",
cover_image: getCoverImage(),
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.",
topics: generateTopics(),
url: "https://bolt.fun/hackathons/shock-the-web"
},
{
id: 4,
title: 'Lightning Startups',
date: '22nd - 28th March, 2022',
location: "Instanbul, Turkey",
cover_image: getCoverImage(),
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.",
topics: generateTopics(),
url: "https://bolt.fun/hackathons/shock-the-web"
},
{
id: 5,
title: 'Design-a-thon',
date: '22nd - 28th March, 2022',
location: "Instanbul, Turkey",
cover_image: getCoverImage(),
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.",
topics: generateTopics(),
url: "https://bolt.fun/hackathons/shock-the-web"
},
{
id: 6,
title: 'Lightning Olympics',
date: '22nd - 28th March, 2022',
location: "Instanbul, Turkey",
cover_image: getCoverImage(),
description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam quam felis ut interdum commodo, scelerisque.",
topics: generateTopics(),
url: "https://bolt.fun/hackathons/shock-the-web"
},
]

View File

@@ -10,6 +10,12 @@ export function randomItem(...args: any[]) {
return args[Math.floor(Math.random() * args.length)];
}
export function randomItems(cnt: number, ...args: any[]) {
console.log(cnt);
return shuffle(args).slice(0, cnt);
}
export function isMobileScreen() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
}
@@ -62,4 +68,23 @@ export function trimText(text: string, length: number) {
export function generateId() {
// TODO: Change to proper generator
return Math.random().toString();
}
}
export function shuffle<T>(_array: Array<T>) {
let array = [..._array]
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle.
while (currentIndex !== 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}