{"id":20381358,"url":"https://github.com/taylormusolf/kickbacker","last_synced_at":"2026-03-04T05:01:45.515Z","repository":{"id":100370471,"uuid":"352819860","full_name":"taylormusolf/KickBacker","owner":"taylormusolf","description":"A clone of Kickstarter, a crowdfunding platform that allows content creators to post their dream projects that they would like others to help bring to life by meeting their funding goal.","archived":false,"fork":false,"pushed_at":"2024-03-28T20:40:27.000Z","size":1836,"stargazers_count":5,"open_issues_count":1,"forks_count":2,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-04T18:34:04.830Z","etag":null,"topics":["clone","crowdfunding","full-stack","postgresql","react","react-redux","ruby-on-rails","user-dashboard"],"latest_commit_sha":null,"homepage":"https://kickbacker.herokuapp.com/","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/taylormusolf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2021-03-30T00:14:45.000Z","updated_at":"2024-10-01T09:09:48.000Z","dependencies_parsed_at":"2024-03-28T21:52:00.919Z","dependency_job_id":null,"html_url":"https://github.com/taylormusolf/KickBacker","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/taylormusolf/KickBacker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taylormusolf%2FKickBacker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taylormusolf%2FKickBacker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taylormusolf%2FKickBacker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taylormusolf%2FKickBacker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/taylormusolf","download_url":"https://codeload.github.com/taylormusolf/KickBacker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/taylormusolf%2FKickBacker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30071895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-04T03:25:38.285Z","status":"ssl_error","status_checked_at":"2026-03-04T03:25:05.086Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clone","crowdfunding","full-stack","postgresql","react","react-redux","ruby-on-rails","user-dashboard"],"created_at":"2024-11-15T02:13:27.696Z","updated_at":"2026-03-04T05:01:45.482Z","avatar_url":"https://github.com/taylormusolf.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align='center'\u003e\u003cimg width=\"400\" alt=\"kickbacker_logo\" src=\"https://user-images.githubusercontent.com/71670060/119078326-d25fba80-b9aa-11eb-8578-35f03d680a2b.PNG\"\u003e\u003c/div\u003e\n\n\nKickBacker, a Kickstarter clone, is a crowdfunding platform that allows content creators to post their dream projects that they would like others to help bring to life by meeting their funding goal. Users can create their own projects or search for and view other users' projects by category as well as back them for a reward.\n\nTry out the app here on [Heroku!](https://kickbacker.herokuapp.com/)\n\nThe KickBacker build utilizes a React/Redux frontend framework integrated with a Ruby on Rails/PostgreSQL backend.\n\n## Technologies:\n\n### Frontend\n* `React` - Open source, component-based JavaScript/UI library\n* `Redux` - Commonly used with React, Redux is also a JavaScript library with a primary function of handling application state\n* `Jquery/Ajax` - JavaScript library used to send promise-based, asynchronous HTTP requests to REST endpoints and perform CRUD operations\n\n### Backend\n* `Ruby on Rails` - Server-side web application framework written in Ruby\n* `PostgreSQL` - Open-source relational database management system emphasizing extensibility and SQL compliance\n* `BCrypt` - Password hashing/salting for user authentication\n* `AWS S3` - Cloud service platform that assists in hosting image assests\n\n## Features:\n* Users can view existing projects while not logged in\n* User Authentication - users can sign up or log in to a corresponding user account\n* Users can create, edit, delete and back projects and rewards if logged in\n* Users can discover new projects through category pages or can search for existing projects\n* User Dashboard that shows all user related projects and access to edit and delete links\n* User status management on project show page checks if user is already a backer, is the project creator or if they are signed in and displays appropriate messages.\n\n\n## Logging In to Back a Project:\n* Incorporated a series of checks on the user/project status to see if the user is eligible to back the project, checking if they were logged in, were the project creator, were already a backer of the project or if the project had ended.\n* In this example the user needs to log in before being able to back a project reward\n\n![backing](https://user-images.githubusercontent.com/71670060/119169565-04a70180-ba17-11eb-9524-716999ca6106.gif)\n\n\n```javascript\n//project_show.jsx\n\n  signedIn(){\n    return this.props.session !== null;\n  }\n\n  isCreator(){\n    return this.props.session === this.props.project.creator.id\n  }\n  projectOver(){\n    return this.daysLeft(this.props.project) === 0;\n  }\n\n  isBacker(){\n    if(this.props.project.backings){\n    const backings = Object.values(this.props.project.backings);\n    let backers = [];\n    \n    backings.forEach((backing)=\u003e{\n      backers.push(backing.backer_id);\n    })\n    return backers.includes(this.props.session)\n    }else{\n      return false\n    }\n  }\n  backerSubmitEligible(){\n    if(this.signedIn() \u0026\u0026 !this.isCreator() \u0026\u0026 !this.projectOver() \u0026\u0026 !this.isBacker()){\n      return(\n        \u003cinput className='reward-support-submit' type=\"submit\" value='Continue'/\u003e\n      )\n    } else {\n      return(\n        \u003cdiv className='reward-support-submit-disabled'\u003eContinue\u003c/div\u003e\n      )\n    }\n    \n  }\n\n  backerMessage(){\n    if(this.isBacker()){\n      return (\u003cdiv\u003eYou backed this project!\u003c/div\u003e)\n    }\n  }\n\n  rewardErrorMessage(){\n    if(this.isCreator()){\n      return (\n        \u003cp className='reward-error'\u003eYou cannot back your own project\u003c/p\u003e\n      )\n    } else if(this.isBacker()){\n      return (\n        \u003cp className='reward-error'\u003eYou have already backed this project\u003c/p\u003e\n      )\n    } else if(!this.signedIn()){\n      return (\n        \u003cp className='reward-error'\u003eYou must be signed in to back a project\u003c/p\u003e\n      )\n    }  \n  }\n```\n\n## Search Backend:\n* User's search query is passed in via :wildcard in frontend URL `/projects/search/:query` which is then mapped within an AJAX request to a backend route of `/api/projects?query=${query}`\n* That AJAX request is routed to the corresponding controller action which in this case is a method called `index`\n* Within this method an ActiveRecord query is run matching against project's title or category name in backend controller.\n* If there are no results or if a query of 'everything' was used, then all projects are returned.\n\n```javascript\n//frontend/util/project_api_util.js\n\nexport const fetchProjects = (query) =\u003e { //function can either receive a query and filter results or receive no query and return \n  let path;\n  if(query){\n    path = `/api/projects?query=${query}`\n  } else {\n    path = `/api/projects`\n  }\n  return $.ajax({\n    method: 'GET',\n    url: path\n  })\n};\n\n```\n\n\n```ruby\n# app/controllers/api/projects_controller.rb\ndef index\n    @projects = Project.all.with_attached_photo.includes({creator: [:projects, :backings]}, :backings, :rewards, :category) #ActiveRecord query that prefetches all projects and corresponding associated data\n    if params[:query] \u0026\u0026 params[:query].downcase != 'everything' #check if there was a query provided and if it wasn't the 'everything' query\n      @projects = @projects.joins(:category).where('projects.title ILIKE (?) or categories.name ILIKE (?)', \"%#{params[:query]}%\", \"%#{params[:query]}%\")\n      #this above query chains off of the one 2 lines above as one query since ActiveRecord Queries are lazy loaded.\n    end\n    render :index\nend\n\n\n```\n\n\n## Search Frontend:\n* User's search query is passed in via :wildcard in frontend URL `/projects/search/:query` and then passed as an argument to `fetchProjects` function.\n* In the below code block you will see how the `SearchPage` component handles fetching the search results and checking if no search matches were found.  \n* If there are no results, user receives a message that no projects were found and all projects are returned.\n\n![search](https://user-images.githubusercontent.com/71670060/119175892-420f8d00-ba1f-11eb-84eb-42ec4d5f0ebf.gif)\n\n```javascript\n\n//search_page.jsx\n\n\ncomponentDidMount(){\n    this.setState({receivedResults: true}) //receivedResults is a state Boolean that confirms we found matching search results. Right now we are assuming we will.\n    this.props.fetchProjects(this.props.query)\n      .then(res =\u003e Object.keys(res.projects).length === 0 \n      ? this.fetchSuggestions() \n      : null);  //we call fetchProjects backend query function with a query argument that comes from the user search and if there are no results we call fetchSuggestions.\n\ncomponentDidUpdate(prevProps){ //works the sames as componentDidMount but is watching for if the query has changed via new user search input\n    if(prevProps.query !== this.props.query){\n      this.setState({receivedResults: true})\n      this.props.fetchProjects(this.props.query).then(res =\u003e Object.keys(res.projects).length === 0 \n      ? this.fetchSuggestions() \n      : null);\n    }\n  }\n\n  fetchSuggestions(){ //if there are no search results we are going to fetch other projects as suggestions to show instead\n    this.props.fetchProjects(); //backend query that will fetch projects\n    this.setState({receivedResults: false}) //we have confirmed no search results returned so we set receivedResults to false\n  }\n  results(){ //function to determine jsx output for the projects we will render\n    const{projects} = this.props; //projects being passed through via mapStateToProps\n    const projectResults = Object.values(projects); //convert object of project objects to an array of project objects\n\n    if(projects){\n      if(this.state.receivedResults){ //condition checking we received matching project results\n        return(\n          \u003csection className='search-results'\u003e\n            \u003ch1\u003eExplore \u003cstrong\u003e{projectResults.length} projects\u003c/strong\u003e\u003c/h1\u003e\n            \u003cdiv className='search-projects-container'\u003e\n              {projectResults.map(project =\u003e (\n                \u003cProjectSearchItem\n                  project={project}\n                  key={[project.id]}\n                /\u003e\n              ))}\n            \u003c/div\u003e\n          \u003c/section\u003e\n        )\n      } else { //alternative condition for the scenario we got no matches and will instead show project suggestions\n        return(\n          \u003cdiv\u003e\n            \u003cdiv className='search-no-results'\u003e\n              \u003ch1\u003e\u003ci className=\"fas fa-exclamation-circle\"\u003e\u003c/i\u003e   We can't find projects that match your search\u003c/h1\u003e\n              \u003ch2\u003eCheck out a collection of popular and recommended options below\u003c/h2\u003e\n            \u003c/div\u003e\n            \u003csection className='search-results'\u003e\n              \u003ch1\u003eExplore \u003cstrong\u003e{projectResults.length} projects\u003c/strong\u003e\u003c/h1\u003e\n              \u003cdiv className='search-projects-container'\u003e\n                {projectResults.map(project =\u003e (\n                  \u003cProjectSearchItem\n                    project={project}\n                    key={[project.id]}\n                  /\u003e\n                ))}\n              \u003c/div\u003e\n            \u003c/section\u003e\n          \u003c/div\u003e\n        )\n      }\n    }\n  }\n\n\n```\n\n## Future Implementations:\n - Project funded and ended features\n - Search dropdown and additional filtering\n - Additional Edit features for Rewards\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftaylormusolf%2Fkickbacker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftaylormusolf%2Fkickbacker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftaylormusolf%2Fkickbacker/lists"}