Skip to content

LABS-EU3/flashcards_backend

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

QuickDecks Backend

"version": "1.0"
"description": "QUICKDECKS REST API"
"apihost":  "https://quickdecks-staging.herokuapp.com/"

Getting Started

These instructions will get you a copy of the project up and running on your local machine for contribution and testing purposes.

Installing

  • Clone Repository $ git clone https://github.com/LABS-EU3/flashcards_backend.git

  • Change Directory $ cd flashcards_backend

  • Install Node Modules $ npm i

  • Setup Environment Variables (.env) on root folder:

    PORT
    DATABASE_URL
    TEST_DATABASE_URL
    NODEMAILER_EMAIL_PASSWORD
    NODEMAILER_EMAIL_ADDRESS
    HOST
    FRONTEND_SITE
    EMAIL_SECRE
    SECRET
    RESET_PASSWORD_REDIRECT
    FRONTEND_SITE
    EMAIL_CONFIRMATION_REDIRECT
    GOOGLE_CLIENT_ID
    GOOGLE_CLIENT_SECRET
    GOOGLE_FRONTEND_REDIRCT
    GOOGLE_BACKEND_BASEUR
    
  • Set up your database $npm run migrate-dev then npm run seed

  • To start API $ npm start or $ npm run server

Running the tests

  • To run tests on api
    • $ npm test
    • or use Postman

Request & Response Examples

Sample Response

Request Success ( 200 - OK || 201 - CREATED )

{
  "message": "Success message",
  "key": "data"
}

Request Error ( 400 - Bad Request || 404 - Not Found || 403 - Unauthorized || 500 - Internal Server Error )

{
  "error": "Error message"
}

API Endpoints

ENDPOINT DESCRIPTION
GET / Base URL
POST /api/auth/register Register new User
POST /api/auth/login Login for User
POST /api/auth/confirm_email Confirm Email for User
POST /api/auth/forgot_password Request reset token
POST /api/auth/reset_password Reset password
GET /api/auth/google/ Redirect to google auth
POST /api/auth/google/:token Confirms auth & login
POST /api/auth/update_password Update password
POST /api/auth/uploadProfile_img Update Profile Image
-------------------------------------------------------------- ----------------------
POST /api/decks Create deck
GET /api/decks All decks of User
GET /api/decks/public all public decks
GET /api/decks/:id View one deck
PUT /api/decks/:id Edit deck
DELETE /api/decks/:id Delete deck
GET /api/decks/favorite Get most used tags
GET /api/decks/access/ 3 decks last accessed
PUT /api/decks/access/:id Update deck access time
DELETE /api/decks/access/:id Remove accessed entry
-------------------------------------------------------------- ----------------------
POST /api/cards Create Flashcard
GET /api/cards All flashcards of User
GET /api/cards/:id View one flashcard
PUT /api/cards/:id Edit flashcard
DELETE /api/cards/:id Delete flashcard
GET /api/cards/COTD Get card of the Day
POST /api/cards/scoring Rate card for user
-------------------------------------------------------------- -----------------------
POST /api/feedback Send feedback
-------------------------------------------------------------- ----------------------
GET /api/users/:id/score Get single user's score
GET /api/users/leaderboard Get top scoring users
PUT /api/users/updateprofile Update Profile
DELETE /api/users Delete User
-------------------------------------------------------------- ----------------------
POST /api/sessions Create session
GET /api/sessions 3 sessions of User
GET /api/sessions/:id View one session
PUT /api/sessions/:id Edit session
DELETE /api/sessions/:id Delete session

GET /

Response body:

{
  "message": "Welcome to the QuickDecks API"
}

Auth

POST /api/auth/register

Description: Creates a new User Account with "isConfirmed": "false" by default.

Request body:

{
  "fullName": "Maaruf Dauda",
  "email": "maaruf@xyz2.com",
  "password": "ALongSecurePassword"
}

Response body:

{
  "message": "User created successfully",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWJqZWN0IjoxMCwibmFtZSI6Ik1hYXJ1ZiBEYXVkYSIsImlhdCI6MTU4MDgwOTYzOSwiZXhwIjoxNTgwODk2MDM5fQ.DWK6gTVBKBR2SFWBVds5oA8o6vfWznxTRVvBTyeKbgo",
    "user": {
      "id": 10,
      "full_name": "Maaruf Dauda",
      "email": "maaruf@xyz2.com",
      "image_url": null,
      "isConfirmed": false,
      "createdon": "2020-02-04T09:47:19.974Z"
    }
  }
}

POST /api/auth/login

Description: Returns an Access token, contains the entire user object as well.

Request body:

{
  "email": "maaruf@xyz2.com",
  "password": "ALongSecurePassword"
}

Response body:

{
  "message": "Welcome. You're logged in!",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWJqZWN0IjoxMCwibmFtZSI6Ik1hYXJ1ZiBEYXVkYSIsImlhdCI6MTU4MDgwOTY4NSwiZXhwIjoxNTgwODk2MDg1fQ.PSLZhEXbvn9uFns5kwFUsSfnT56zk9B3cvAd_ocm2oA",
    "user": {
      "id": 10,
      "full_name": "Maaruf Dauda",
      "email": "maaruf@xyz2.com",
      "isConfirmed": false,
      "createdon": "2020-02-04T09:47:19.974Z"
    }
  }
}

POST /api/auth/confirm_email

Description: Confirms Email for a User. Email token must be passed in. Returns an Acces token for the user..

Request body:

{
  "token": "anEmailTokenYouShouldNotBotherDecryprting.eyJzdWJqZWN0IjADfe3KLo98IjoiTWFhcnVmIERhdWRhIiwiaWF0IjoxNTc2NzYzNzA0LCJleHAiOjE1NzY4NTAxMDR9.jsihrtPG37mKBHp3xvjrQ-UselessRjSMr5YlPovG5A"
}

Response body:

{
  "message": "User with email: maaruf@xyz.com confirmed.",
  "token": "aTokenYouShouldNotBotherDecryprting.eyJzdWJqZWN0IjADfe3KLo98IjoiTWFhcnVmIERhdWRhIiwiaWF0IjoxNTc2NzYzNzA0LCJleHAiOjE1NzY4NTAxMDR9.jsihrtPG37mKBHp3xvjrQ-UselessRjSMr5YlPovG5A"
}

POST /api/auth/forgot_password

Description: Begins password reset process by sending email token to the user's registered email..

Request body:

{
  "email": "anna@xyz.com"
}

Response body:

{
  "message": "Email sent to user"
}

POST /api/auth/reset_Password

Description: Completes password reset process. Changes user's saved password. Passwords must match..

Request body:

{
  "password": "toldYouSecurityIsNotAFad",
  "confirmPassword": "toldYouSecurityIsNotAFad"
}

Response body:

{
  "message": "Password reset successfully."
}

GET /api/auth/google

Description: Redirects user to google auth, user will signin or cannot and will be redirected back to the landing page..

Request body:

{}

Response body:

{}

POST /api/auth/google/:token

Description: User will be verified in the data based as created and will be sent a token with userID..

Request body:

{}
{
  "token": "aTokenYouShouldNotBotherDecryprting.eyJzdWJqZWN0IjADfe3KLo98IjoiTWFhcnVmIERhdWRhIiwiaWF0IjoxNTc2NzYzNzA0LCJleHAiOjE1NzY4NTAxMDR9.jsihrtPG37mKBHp3xvjrQ-UselessRjSMr5YlPovG5A",
  "user": {
    "id": 1,
    "full_name": "Anna",
    "email": "anna@xyz.com",
    "image_url": null,
    "isConfirmed": false,
    "createdon": "2020-02-04T09:47:19.974Z"
  }
}

POST /api/auth/update_Password

Description: Updates a user's password..

Request body:

{
  "newPassword": "my new password",
  "confirmPassword": "matches my new password"
}
{
  "message": "Password updated successfully"
}

POST api/auth/uploadProfile_img

Description: It stores image url on the db(users table)..

Request body:

{
  "imageUrl": "this-is-a-test"
}

Response body:

{
  "message": "Image url stored successfully"
}

POST api/users/updateprofile

Description: It updates users profile on the db(users table)..

Request body:

{
  "fullName": "updated fullName"
}

Response body:

{
  "message": "Profile updated successfully"
}

Decks

POST /api/decks

Description: Creates a deck..

Request body:

{
  "name": "New Decks",
  "tags": [1, 2, 3, 4]
}

Required: name: string, tags: [int],

Response body:

{
  "deck": {
    "id": 17,
    "user_id": 10,
    "name": "New Decks",
    "public": false,
    "created_at": "2020-02-04T09:49:52.265Z",
    "updated_at": "2020-02-04T09:49:52.265Z"
  }
}

GET /api/decks

Description: Retrieves all decks made by a specific User..

Request body:

{}

Response body:

{
  "data": [
    {
      "deck_id": 17,
      "user_id": 10,
      "deck_name": "New Decks",
      "public": false,
      "created_at": "2020-02-04T09:49:52.265Z",
      "updated_at": "2020-02-04T09:49:52.265Z",
      "tags": [
        {
          "id": 1,
          "name": "Accounting & Finance"
        },
        {
          "id": 2,
          "name": "Aeronautical & Manufacturing Engineering"
        },
        {
          "id": 3,
          "name": "Agriculture & Forestry"
        },
        {
          "id": 4,
          "name": "American Studies"
        }
      ],
      "flashcards": [null]
    }
  ]
}

GET /api/decks/public

Description: Get users private decks and all public decks.

Request body:

{}

Response body:

{
  "data": [
    {
      "deck_id": 3,
      "user_id": 3,
      "deck_name": "Technology ",
      "public": true,
      "created_at": "2020-02-03T09:21:15.181Z",
      "updated_at": "2020-02-03T09:21:15.181Z",
      "tags": [
        {
          "id": 6,
          "name": "Anthropology"
        },
        {
          "id": 7,
          "name": "Archaeology"
        }
      ],
      "flashcards": [
        {
          "id": 5,
          "deck_id": 3,
          "user_id": 3,
          "question": "In which decade was the American Institute of Electrical Engineers (AIEE) founded?",
          "answer": "1880s",
          "image_url_question": null,
          "created_at": "2020-02-03T04:21:15.188077-05:00",
          "updated_at": "2020-02-03T04:21:15.188077-05:00",
          "image_url_answer": null
        },
        {
          "id": 6,
          "deck_id": 3,
          "user_id": 3,
          "question": "What is part of a database that holds only one type of information?",
          "answer": "Field",
          "image_url_question": null,
          "created_at": "2020-02-03T04:21:15.188077-05:00",
          "updated_at": "2020-02-03T04:21:15.188077-05:00",
          "image_url_answer": null
        }
      ]
    },
    {
      "deck_id": 4,
      "user_id": 4,
      "deck_name": "Biology ",
      "public": true,
      "created_at": "2020-02-03T09:21:15.181Z",
      "updated_at": "2020-02-03T09:21:15.181Z",
      "tags": [
        {
          "id": 8,
          "name": "Architecture"
        },
        {
          "id": 9,
          "name": "Art & Design"
        }
      ],
      "flashcards": [
        {
          "id": 7,
          "deck_id": 4,
          "user_id": 4,
          "question": "Ordinary table salt is sodium chloride. What is baking soda?",
          "answer": "Sodium bicarbonate",
          "image_url_question": null,
          "created_at": "2020-02-03T04:21:15.188077-05:00",
          "updated_at": "2020-02-03T04:21:15.188077-05:00",
          "image_url_answer": null
        },
        {
          "id": 8,
          "deck_id": 4,
          "user_id": 4,
          "question": "Plants receive their nutrients from the?",
          "answer": "Sun",
          "image_url_question": null,
          "created_at": "2020-02-03T04:21:15.188077-05:00",
          "updated_at": "2020-02-03T04:21:15.188077-05:00",
          "image_url_answer": null
        }
      ]
    },
    {
      "deck_id": 17,
      "user_id": 10,
      "deck_name": "New Decks",
      "public": false,
      "created_at": "2020-02-04T09:49:52.265Z",
      "updated_at": "2020-02-04T09:49:52.265Z",
      "tags": [
        {
          "id": 1,
          "name": "Accounting & Finance"
        },
        {
          "id": 2,
          "name": "Aeronautical & Manufacturing Engineering"
        },
        {
          "id": 3,
          "name": "Agriculture & Forestry"
        },
        {
          "id": 4,
          "name": "American Studies"
        }
      ],
      "flashcards": [null]
    }
  ]
}

GET /api/decks/:id

Description: Edit a deck by deck Id..

Request body:

{}

Response body:

{
  "deck": {
    "deck_id": 6,
    "user_id": 6,
    "deck_name": "Famous Personalities",
    "public": true,
    "created_at": "2020-01-13T15:49:59.080Z",
    "updated_at": "2020-01-13T15:49:59.080Z",
    "tags": [
      {
        "id": 12,
        "name": "Building"
      },
      {
        "id": 13,
        "name": "Business & Management Studies"
      }
    ],
    "flashcards": [
      {
        "id": 11,
        "deck_id": 6,
        "user_id": 6,
        "question": "Who is the father of Geometry?",
        "answer": "Euclid",
        "image_url_question": null,
        "created_at": "2020-01-13T10:49:59.086613-05:00",
        "updated_at": "2020-01-13T10:49:59.086613-05:00",
        "image_url_answer": null
      },
      {
        "id": 12,
        "deck_id": 6,
        "user_id": 6,
        "question": "The Indian to beat the computers in mathematical wizardry is",
        "answer": "Shakunthala Devi",
        "image_url_question": null,
        "created_at": "2020-01-13T10:49:59.086613-05:00",
        "updated_at": "2020-01-13T10:49:59.086613-05:00",
        "image_url_answer": null
      }
    ]
  }
}

PUT /api/decks/:id

Description: Edit a deck..

Request body:

{
  "name": "New Decks",
  "addTags": [15],
  "removeTags": [3]
}

Response body:

{
  "deck_id": 17,
  "user_id": 10,
  "deck_name": "New Decks",
  "public": false,
  "created_at": "2020-02-04T09:49:52.265Z",
  "updated_at": "2020-02-04T09:49:52.265Z",
  "tags": [
    {
      "id": 1,
      "name": "Accounting & Finance"
    },
    {
      "id": 2,
      "name": "Aeronautical & Manufacturing Engineering"
    },
    {
      "id": 4,
      "name": "American Studies"
    },
    {
      "id": 12,
      "name": "Building"
    },
    {
      "id": 15,
      "name": "Chemical Engineering"
    }
  ],
  "flashcards": [null]
}

DELETE /api/decks/:id

Description: Delete a deck..

Request body:

{}

Response body:

{}

GET /api/decks/access/

Description: Get users last accessed decks..

Request body:

{}

Response body:

{
  "data": [
    {
      "deck_id": 17,
      "user_id": 10,
      "deck_name": "New Decks",
      "public": false,
      "created_at": "2020-02-04T09:49:52.265Z",
      "updated_at": "2020-02-04T09:49:52.265Z",
      "accessed_time": "2020-02-04T09:49:52.295Z",
      "flashcards": [null]
    }
  ]
}

PUT /api/decks/access/:id

Description: Update access time on a users deck id being the deck id..

Request body:

{}

Response body:

{}

DELETE /api/decks/access/:id

Description: Delete access connection from user to deck id being the deck id..

Request body:

{}

Response body:

{}

GET /api/decks/favorite

Description: Get favorite tags..

Request body:

{}

Response body:

[
  {
    "name": "Accounting & Finance",
    "value_occurrence": "3"
  },
  {
    "name": "Aeronautical & Manufacturing Engineering",
    "value_occurrence": "2"
  },
  {
    "name": "Agriculture & Forestry",
    "value_occurrence": "1"
  },
  {
    "name": "American Studies",
    "value_occurrence": "1"
  },
  {
    "name": null,
    "value_occurrence": "0"
  }
]

Flashcards

POST /api/cards/

Description: Creates a flashcard in a deck..

Request body:

{
  "deckId": 2,
  "questionText": "How do I create a flashcard",
  "answerText": "Post to /api/card",
  "imageUrlQuestion": "www.realurl.com",
  "imageUrlAnswer": "www.google.com"
}

Required: deckId: int, userId: int, questionText: String, answertText: String

Response body:

{
  "id": 20,
  "deck_id": 2,
  "user_id": 2,
  "question": "How do I create a flashcard",
  "answer": "Post to /api/card",
  "image_url_question": "www.realurl.com",
  "created_at": "2020-01-22T13:47:57.348Z",
  "updated_at": "2020-01-22T13:47:57.348Z",
  "image_url_answer": "www.google.com"
}

GET /api/cards/

Description: Retrieves all flashcards made by a specific User..

Request body:

{}

Response body:

[
  {
    "id": 2,
    "deck_id": 1,
    "user_id": 1,
    "question": "here is my question answer me",
    "answer": "here is my answer question me",
    "image_url_question": null,
    "created_at": "2020-01-08T10:44:38.761Z",
    "updated_at": "2020-01-08T10:44:38.761Z",
    "image_url_answer": null
  },
  {
    "id": 3,
    "deck_id": 1,
    "user_id": 1,
    "question": "here is my question answer me",
    "answer": "here is my answer question me",
    "image_url_question": null,
    "created_at": "2020-01-08T10:45:05.269Z",
    "updated_at": "2020-01-08T10:45:05.269Z",
    "image_url_answer": null
  },
  {
    "id": 5,
    "deck_id": 1,
    "user_id": 1,
    "question": "here is my question answer me",
    "answer": "here is my answer question me",
    "image_url_question": null,
    "created_at": "2020-01-08T11:34:52.174Z",
    "updated_at": "2020-01-08T11:34:52.174Z",
    "image_url_answer": null
  }
]

GET /api/cards/:id

Description: Retrieves a specific card by the card's id..

Request body:

{}

Response body:

{
  "id": 3,
  "deck_id": 1,
  "user_id": 1,
  "question": "here is my question answer me",
  "answer": "here is my answer question me",
  "image_url_question": null,
  "created_at": "2020-01-08T10:45:05.269Z",
  "updated_at": "2020-01-08T10:45:05.269Z",
  "image_url_answer": null
}

PUT /api/cards/:id

Description: Edit a flashcard by flashcard Id..

Request body:

{
  "deckId": 2,
  "questionText": "updated question",
  "answerText": "updated question",
  "imageUrlQuestion": "www.gify.com/image",
  "imageUrlAnswer": "www.google.com"
}

Response body:

{
  "id": 17,
  "deck_id": 2,
  "user_id": 2,
  "question": "updated question",
  "answer": "updated question",
  "image_url_question": "www.gify.com/image",
  "created_at": "2020-01-22T13:14:26.879Z",
  "updated_at": "2020-01-22T13:14:26.879Z",
  "image_url_answer": "www.google.com"
}

DELETE /api/cards/:id

Description: Delete a flashcard..

Request body:

{}

Response body:

{}

GET /api/cards/COTD

Description: Get Card of The Day..

Request body:

{}

Response body:

{
  "id": 3,
  "deck_id": 1,
  "user_id": 1,
  "question": "here is my question answer me",
  "answer": "here is my answer question me",
  "image_url_question": null,
  "created_at": "2020-01-08T10:45:05.269Z",
  "updated_at": "2020-01-08T10:45:05.269Z",
  "image_url_answer": null
}

POST /api/cards/scoring

Description: Score single flashcard when user in study mode..

Request body:

{
  "card_id": 1,
  "session_id": 3,
  "rating": 5
}

Response body:

{
  "message": "Successfully scored"
}

Users

GET api/users/:id/score

Description: Get a single user's score..

Request body:

{}

Response body:

{
  "message": "Successfully fetched User score",
  "data": {
    "score": 10,
    "email": "maaruf@xyz.com",
    "full_name": "John Doe",
    "id": 8
  }
}

GET api/users/leaderboard

Description: Get top 15 scoring users..

Request body:

{}

Response body:

{
  "message": "Fetched leaderboard",
  "data": [
    {
      "user_id": 8,
      "full_name": "Maaruf Doe",
      "email": "maaruf@xyz.com",
      "score": 10
    },
    {
      "user_id": 9,
      "full_name": "Anna Doe",
      "email": "anna@xyz.com",
      "score": 5
    }
  ]
}

DELETE /api/users/

Description: Delete a user..

Request body:

{
  "password": "this-is-a-password"
}

Response body:

{
  "message": "User successfully deleted"
}

POST /api/feedback

Description: Sends user feedback to the user's email and the QuickDecks email.

Request body:

{
  "feedback": "Hello there. I love using this app - its so great! Can I send this feedback to you?"
}

Response body

{
  "message": "User feedback sent successfully",
  "data": {
    "feedback": "Hello there. I love using this app - its so great! Can I send this feedback to you?"
  }
}

Required: feedback: string

Sessions

POST /api/sessions/

Request body:

{
  "deckId": 5
}

Response body:

{
  "session": {
    "id": 24,
    "deck_id": 8,
    "user_id": 5,
    "isCompleted": false,
    "last_used": "2020-01-30T09:23:33.275Z",
    "reviewed_cards": [null],
    "flashcards": [null]
  }
}

Description: Creates a session..

GET /api/sessions/

Description: Retrieves all sessions made by a specific User.

Request body:

{}

Response body:

{
  "data": [
    {
      "id": 4,
      "name": "Biology ",
      "deck_id": 4,
      "user_id": 4,
      "isCompleted": false,
      "last_used": "2020-02-04T10:05:26.158Z",
      "reviewed_cards": [
        {
          "id": 4,
          "session_id": 4,
          "card_id": 7
        }
      ],
      "flashcards": [
        {
          "id": 7,
          "deck_id": 4,
          "user_id": 4,
          "question": "Ordinary table salt is sodium chloride. What is baking soda?",
          "answer": "Sodium bicarbonate",
          "image_url_question": null,
          "created_at": "2020-02-04T05:05:26.135926-05:00",
          "updated_at": "2020-02-04T05:05:26.135926-05:00",
          "image_url_answer": null
        },
        {
          "id": 8,
          "deck_id": 4,
          "user_id": 4,
          "question": "Plants receive their nutrients from the?",
          "answer": "Sun",
          "image_url_question": null,
          "created_at": "2020-02-04T05:05:26.135926-05:00",
          "updated_at": "2020-02-04T05:05:26.135926-05:00",
          "image_url_answer": null
        }
      ],
      "cards_left": 1
    }
  ]
}

GET /api/sessions/:id

Description: Retrieves a specific session by the session's id..

Request body:

{}

Response body:

{
  "session": {
    "id": 5,
    "deck_id": 5,
    "user_id": 5,
    "isCompleted": false,
    "last_used": "2020-01-29T15:39:24.363Z",
    "reviewed_cards": [
      {
        "id": 5,
        "session_id": 5,
        "card_id": 9
      }
    ]
  }
}

PUT /api/sessions/:id

Description: Edit a session by session Id, you can also send a blank request just to update lastused, when send "isCompleted": true, it will remove the session completely ..

Request body:

{
  
  "cardIds": [10]
}

Response body:

{
  "session": {
    "id": 6,
    "deck_id": 6,
    "user_id": 6,
    "isCompleted": false,
    "last_used": "2020-01-29T15:39:24.363Z",
    "reviewed_cards": [
      {
        "id": 6,
        "session_id": 6,
        "card_id": 11
      }
    ],
    "flashcards": [
      {
        "id": 11,
        "deck_id": 6,
        "user_id": 6,
        "question": "Who is the father of Geometry?",
        "answer": "Euclid",
        "image_url_question": null,
        "created_at": "2020-01-29T10:39:24.340918-05:00",
        "updated_at": "2020-01-29T10:39:24.340918-05:00",
        "image_url_answer": null
      },
      {
        "id": 12,
        "deck_id": 6,
        "user_id": 6,
        "question": "The Indian to beat the computers in mathematical wizardry is",
        "answer": "Shakunthala Devi",
        "image_url_question": null,
        "created_at": "2020-01-29T10:39:24.340918-05:00",
        "updated_at": "2020-01-29T10:39:24.340918-05:00",
        "image_url_answer": null
      }
    ],
    "cards_left": 1
  }
}

DELETE /api/sessions/:id

Description: Delete a session..

Request body:

{}

Response body:

{}

The Tag Data We Used

Seed Data

Technologies

  • Node | @hapi/joi | bcrypt | cors | crypto | dotenv | express | helmet | jsonwebtoken | knex | mailgen |nodemailer | nodemailer-stub | passport | passport-google-oauth | pg

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published