mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-21 22:44:21 +01:00
feat: hackthons page componets, stories api
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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!
|
||||
}
|
||||
51
functions/graphql/types/hackathon.js
Normal file
51
functions/graphql/types/hackathon.js
Normal 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,
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 = {
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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']
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 = {
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
13
src/features/Hackathons/types/hackathons.interface.ts
Normal file
13
src/features/Hackathons/types/hackathons.interface.ts
Normal 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
|
||||
}
|
||||
1
src/features/Hackathons/types/index.ts
Normal file
1
src/features/Hackathons/types/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './hackathons.interface'
|
||||
@@ -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
|
||||
}
|
||||
94
src/mocks/data/hackathon.ts
Normal file
94
src/mocks/data/hackathon.ts
Normal 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"
|
||||
},
|
||||
]
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user