diff --git a/functions/graphql/nexus-typegen.ts b/functions/graphql/nexus-typegen.ts index 0e00729..c8fd0a7 100644 --- a/functions/graphql/nexus-typegen.ts +++ b/functions/graphql/nexus-typegen.ts @@ -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 diff --git a/functions/graphql/schema.graphql b/functions/graphql/schema.graphql index cdeed98..5fefd58 100644 --- a/functions/graphql/schema.graphql +++ b/functions/graphql/schema.graphql @@ -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! } \ No newline at end of file diff --git a/functions/graphql/types/hackathon.js b/functions/graphql/types/hackathon.js new file mode 100644 index 0000000..ffc0f1b --- /dev/null +++ b/functions/graphql/types/hackathon.js @@ -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, +} \ No newline at end of file diff --git a/functions/graphql/types/post.js b/functions/graphql/types/post.js index b031159..6bf54bf 100644 --- a/functions/graphql/types/post.js +++ b/functions/graphql/types/post.js @@ -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 diff --git a/functions/graphql/types/vote.js b/functions/graphql/types/vote.js index cb79cfd..d46b58b 100644 --- a/functions/graphql/types/vote.js +++ b/functions/graphql/types/vote.js @@ -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 } \ No newline at end of file diff --git a/prisma/migrations/20220519165912_add_stories_questions_users/migration.sql b/prisma/migrations/20220519165912_add_stories_questions_users/migration.sql new file mode 100644 index 0000000..1120c4c --- /dev/null +++ b/prisma/migrations/20220519165912_add_stories_questions_users/migration.sql @@ -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; diff --git a/prisma/migrations/20220519171237_add_created_at_updated_at_to_stories_questions/migration.sql b/prisma/migrations/20220519171237_add_created_at_updated_at_to_stories_questions/migration.sql new file mode 100644 index 0000000..d2b44df --- /dev/null +++ b/prisma/migrations/20220519171237_add_created_at_updated_at_to_stories_questions/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index fcd9508..83aac32 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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[] } diff --git a/src/Components/Button/Button.tsx b/src/Components/Button/Button.tsx index 7e83e7d..69cf207 100644 --- a/src/Components/Button/Button.tsx +++ b/src/Components/Button/Button.tsx @@ -21,11 +21,11 @@ interface Props { const btnStylesFill: UnionToObjectKeys = { 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 = { @@ -38,7 +38,7 @@ const btnStylesOutline: UnionToObjectKeys = { } const baseBtnStyles: UnionToObjectKeys = { - 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]} diff --git a/src/Components/Navbar/Navbar.tsx b/src/Components/Navbar/Navbar.tsx index 638fc3f..df31f44 100644 --- a/src/Components/Navbar/Navbar.tsx +++ b/src/Components/Navbar/Navbar.tsx @@ -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]) diff --git a/src/features/Hackathons/Components/HackathonCard/HackathonCard.Skeleton.tsx b/src/features/Hackathons/Components/HackathonCard/HackathonCard.Skeleton.tsx new file mode 100644 index 0000000..0f2fb11 --- /dev/null +++ b/src/features/Hackathons/Components/HackathonCard/HackathonCard.Skeleton.tsx @@ -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 ( +
+
+
+
+

+ +

+

+ +

+

+ +

+

+ + +

+
+
+
+
+
+
+
+
+
+
+
+ ) +} diff --git a/src/features/Hackathons/Components/HackathonCard/HackathonCard.stories.tsx b/src/features/Hackathons/Components/HackathonCard/HackathonCard.stories.tsx new file mode 100644 index 0000000..c80cc89 --- /dev/null +++ b/src/features/Hackathons/Components/HackathonCard/HackathonCard.stories.tsx @@ -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; + + +const Template: ComponentStory = (args) =>
+ +export const Default = Template.bind({}); +Default.args = { + hackathon: MOCK_DATA['hackathons'][0] +} + + + + +const LoadingTemplate: ComponentStory = (args) =>
+ +export const Loading = LoadingTemplate.bind({}); +Loading.args = { + +} \ No newline at end of file diff --git a/src/features/Hackathons/Components/HackathonCard/HackathonCard.tsx b/src/features/Hackathons/Components/HackathonCard/HackathonCard.tsx new file mode 100644 index 0000000..fc5f545 --- /dev/null +++ b/src/features/Hackathons/Components/HackathonCard/HackathonCard.tsx @@ -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 ( +
+ +
+
+

+ {hackathon.title} +

+

+ {hackathon.date} +

+

+ {hackathon.location} +

+

+ {hackathon.description} +

+
+
+ {hackathon.topics.map(topic =>
{topic.title}
)} + +
+ +
+
+ ) +} diff --git a/src/features/Hackathons/Components/HackathonsList/HackathonsList.stories.tsx b/src/features/Hackathons/Components/HackathonsList/HackathonsList.stories.tsx new file mode 100644 index 0000000..f8c9a98 --- /dev/null +++ b/src/features/Hackathons/Components/HackathonsList/HackathonsList.stories.tsx @@ -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; + + +const Template: ComponentStory = (args) => + +export const Default = Template.bind({}); +Default.args = { + items: MOCK_DATA['hackathons'] +} + + diff --git a/src/features/Hackathons/Components/HackathonsList/HackathonsList.tsx b/src/features/Hackathons/Components/HackathonsList/HackathonsList.tsx new file mode 100644 index 0000000..ae54874 --- /dev/null +++ b/src/features/Hackathons/Components/HackathonsList/HackathonsList.tsx @@ -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 + +export default function HackathonsList(props: Props) { + + const { ref } = useReachedBottom(props.onReachedBottom) + + if (props.isLoading) + return
+ {<> + + + + + } +
+ + return ( +
+ { + props.items?.map(hackathon => ) + } + {props.isFetching && } +
+ ) +} diff --git a/src/features/Hackathons/Components/SortByFilter/SortByFilter.stories.tsx b/src/features/Hackathons/Components/SortByFilter/SortByFilter.stories.tsx new file mode 100644 index 0000000..03d3f85 --- /dev/null +++ b/src/features/Hackathons/Components/SortByFilter/SortByFilter.stories.tsx @@ -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; + + +const Template: ComponentStory = (args) =>
+ +export const Default = Template.bind({}); +Default.args = { +} + + diff --git a/src/features/Hackathons/Components/SortByFilter/SortByFilter.tsx b/src/features/Hackathons/Components/SortByFilter/SortByFilter.tsx new file mode 100644 index 0000000..66ae958 --- /dev/null +++ b/src/features/Hackathons/Components/SortByFilter/SortByFilter.tsx @@ -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 ( +
+

Sort By

+
    + {filters.map((f, idx) =>
  • filterClicked(f.value)} + > + {f.text} +
  • )} +
+
+ ) +} diff --git a/src/features/Hackathons/Components/TopicsFilter/TopicsFilter.stories.tsx b/src/features/Hackathons/Components/TopicsFilter/TopicsFilter.stories.tsx new file mode 100644 index 0000000..d8c9eb7 --- /dev/null +++ b/src/features/Hackathons/Components/TopicsFilter/TopicsFilter.stories.tsx @@ -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; + + +const Template: ComponentStory = (args) =>
+ +export const Default = Template.bind({}); +Default.args = { +} + + diff --git a/src/features/Hackathons/Components/TopicsFilter/TopicsFilter.tsx b/src/features/Hackathons/Components/TopicsFilter/TopicsFilter.tsx new file mode 100644 index 0000000..79e7155 --- /dev/null +++ b/src/features/Hackathons/Components/TopicsFilter/TopicsFilter.tsx @@ -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 ( +
+

Topics

+
    + {filters.map((f, idx) =>
  • filterClicked(f.value)} + > + {f.icon} + + {f.text} + +
  • )} +
+
+ ) +} diff --git a/src/features/Hackathons/pages/HackathonsPage/HackathonsPage.tsx b/src/features/Hackathons/pages/HackathonsPage/HackathonsPage.tsx new file mode 100644 index 0000000..f6a295b --- /dev/null +++ b/src/features/Hackathons/pages/HackathonsPage/HackathonsPage.tsx @@ -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 ( +
+ + +
+ ) +} diff --git a/src/features/Hackathons/pages/HackathonsPage/styles.module.scss b/src/features/Hackathons/pages/HackathonsPage/styles.module.scss new file mode 100644 index 0000000..92cd659 --- /dev/null +++ b/src/features/Hackathons/pages/HackathonsPage/styles.module.scss @@ -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); + } +} diff --git a/src/features/Hackathons/types/hackathons.interface.ts b/src/features/Hackathons/types/hackathons.interface.ts new file mode 100644 index 0000000..c45d2bf --- /dev/null +++ b/src/features/Hackathons/types/hackathons.interface.ts @@ -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 +} \ No newline at end of file diff --git a/src/features/Hackathons/types/index.ts b/src/features/Hackathons/types/index.ts new file mode 100644 index 0000000..7994227 --- /dev/null +++ b/src/features/Hackathons/types/index.ts @@ -0,0 +1 @@ +export * from './hackathons.interface' \ No newline at end of file diff --git a/src/mocks/data.ts b/src/mocks/data.ts index 42e78c7..42acbda 100644 --- a/src/mocks/data.ts +++ b/src/mocks/data.ts @@ -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 } \ No newline at end of file diff --git a/src/mocks/data/hackathon.ts b/src/mocks/data/hackathon.ts new file mode 100644 index 0000000..e4026b5 --- /dev/null +++ b/src/mocks/data/hackathon.ts @@ -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" + }, +] \ No newline at end of file diff --git a/src/utils/helperFunctions.tsx b/src/utils/helperFunctions.tsx index 7af4a09..6eee3eb 100644 --- a/src/utils/helperFunctions.tsx +++ b/src/utils/helperFunctions.tsx @@ -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(); -} \ No newline at end of file +} + +export function shuffle(_array: Array) { + 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; +}