locale_ninja 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +93 -0
  4. data/Rakefile +10 -0
  5. data/app/assets/builds/locale_ninja.css +1 -0
  6. data/app/assets/config/locale_ninja_manifest.js +1 -0
  7. data/app/assets/stylesheets/application.tailwind.css +17 -0
  8. data/app/assets/stylesheets/locale_ninja/application.css +26 -0
  9. data/app/controllers/locale_ninja/application_controller.rb +35 -0
  10. data/app/controllers/locale_ninja/branches_controller.rb +22 -0
  11. data/app/controllers/locale_ninja/dashboard_controller.rb +19 -0
  12. data/app/controllers/locale_ninja/locales_controller.rb +33 -0
  13. data/app/controllers/locale_ninja/session_controller.rb +34 -0
  14. data/app/helpers/locale_ninja/application_helper.rb +9 -0
  15. data/app/helpers/locale_ninja/dashboard_helper.rb +6 -0
  16. data/app/helpers/locale_ninja/locale_helper.rb +91 -0
  17. data/app/jobs/locale_ninja/application_job.rb +6 -0
  18. data/app/mailers/locale_ninja/application_mailer.rb +8 -0
  19. data/app/models/locale_ninja/application_record.rb +7 -0
  20. data/app/services/locale_ninja/github_api_service.rb +98 -0
  21. data/app/views/components/_branch_select.html.erb +10 -0
  22. data/app/views/components/_flashes.html.erb +33 -0
  23. data/app/views/components/_sidebar.html.erb +31 -0
  24. data/app/views/components/_table.html.erb +204 -0
  25. data/app/views/layouts/locale_ninja/application.html.erb +15 -0
  26. data/app/views/locale_ninja/branches/index.html.erb +1 -0
  27. data/app/views/locale_ninja/branches/show.html.erb +22 -0
  28. data/app/views/locale_ninja/dashboard/index.html.erb +34 -0
  29. data/app/views/locale_ninja/locales/show.html.erb +21 -0
  30. data/config/credentials/development.key +1 -0
  31. data/config/credentials/development.yml.enc +1 -0
  32. data/config/credentials/production.yml.enc +1 -0
  33. data/config/locales/locale_ninja/en.yml +6 -0
  34. data/config/locales/locale_ninja/fr.yml +6 -0
  35. data/config/routes.rb +13 -0
  36. data/config/tailwind.config.js +23 -0
  37. data/lib/locale_ninja/engine.rb +11 -0
  38. data/lib/locale_ninja/version.rb +6 -0
  39. data/lib/locale_ninja.rb +8 -0
  40. data/lib/tasks/locale_ninja_tasks.rake +21 -0
  41. metadata +190 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e9cb6c35b765aebbf37151c45621097c3de4f820e11460a44907b156b9b67d9f
4
+ data.tar.gz: e303d7e92adb1ecc59760bde0556385e899127e557a834722c3604ce27233570
5
+ SHA512:
6
+ metadata.gz: f0cea58d0d9c1428cce05e007c1b6988e1b47fd5847fa053b8c76c83f68dd56adebe07aedc3009792ea0f9718c99f4c0a62fe2810333d13af905e0bea126c11c
7
+ data.tar.gz: b02b41469d821255b749fbebb7e39d41ef03fe46c6c5152705f43de9ceb76d4f7a4b87136d42bfa7ba509836578aab4595c8449bf49f377a9fcba126c76b52bb
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2023 Squadracer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,93 @@
1
+ [![Gem Version](https://badge.fury.io/rb/locale_ninja.svg)](https://badge.fury.io/rb/locale_ninja)
2
+ [![Ruby on Rails CI](https://github.com/squadracer/locale_ninja/actions/workflows/rubyonrails.yml/badge.svg)](https://github.com/squadracer/locale_ninja/actions/workflows/rubyonrails.yml)
3
+
4
+ # 🥷 LocaleNinja
5
+
6
+ A Git-based gem to manage translations in your Ruby on Rails app.
7
+
8
+ LocaleNinja simplifies the management of translations on a website. Unlike traditional solutions that require connecting to an external platform, LocaleNinja is a Git-based gem installed directly in your project, allowing you to maintain full control over your translations without relying on a third-party service.
9
+
10
+ <br/>
11
+
12
+ ## ✨ Key Features
13
+ **Streamlined Translation Management:** LocaleNinja provides a user-friendly interface to effortlessly handle all your website translations within the same project.
14
+
15
+ **Seamless Git Integration:** LocaleNinja connects to your Git repository and automatically handles pull and push of translation files. This ensures smooth collaboration with developers and simplifies the process of updating translations.
16
+
17
+ <br/>
18
+
19
+ ## 💻 Installation
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem "locale_ninja"
24
+ ```
25
+
26
+ And then execute:
27
+ ```bash
28
+ $ bundle
29
+ ```
30
+
31
+ Or install it yourself as:
32
+ ```bash
33
+ $ gem install locale_ninja
34
+ ```
35
+
36
+ <br/>
37
+
38
+ ## ⚙️ Setup
39
+
40
+ To setup LocalNinja you will need to create a [github app](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app) in your repository, it will allow your app to commit to your repo. When you are on the github app form, here are some steps specific to our application to follow :
41
+
42
+ - In the "Identifying and authorizing users" section, your callback url will be : `your-domain-name.com/locale_ninja/github`
43
+ - In the "Webhook" section switch off the "active" checkbox
44
+ - In the "Permissions" section, you will have to:
45
+ - Switch "Content" permissions to "Read and write"
46
+ - Switch "Metadata" permissions to "Read-only"
47
+ - Switch "Pull requests" permissions to "Read-only"
48
+
49
+ <br/>
50
+
51
+ Once done you will have access to your `client_id` and `client_secret`. You can then run :
52
+
53
+ ```sh
54
+ bin/rails credentials:edit
55
+ ```
56
+
57
+ And add this, in the editor that just open-up :
58
+ ```yaml
59
+ github:
60
+ repository_name: organization/repository_name
61
+ client_secret: <40 bytes long secret key>
62
+ client_id: <20 bytes long id>
63
+ ```
64
+
65
+ You can then close the editor, this will generate a `master.key`file and a `credentials.yml.enc` file in the config folder. Now your connexion with github is totally setup.
66
+
67
+ <br/>
68
+
69
+ You now just have to add this in your routes :
70
+ ```ruby
71
+ #config/routes.rb
72
+ mount LocaleNinja::Engine => '/locale_ninja'
73
+ ```
74
+
75
+ <br/>
76
+
77
+ Your translation manager will be accessible at `your-domain-name/locale_ninja` or `localhost:3000/locale_ninja` 🎉
78
+
79
+ ## 👥 Contributors
80
+
81
+ <table>
82
+ <tbody>
83
+ <tr>
84
+ <td align="center" valign="top" width="25%"><a href="https://twitter.com/julienmarseil"><img src="https://avatars.githubusercontent.com/u/18447285?v=4" width="100px;" alt="Julien Marseille"/><br /><sub><b>Julien Marseille</b></sub></a></td>
85
+ <td align="center" valign="top" width="25%"><a href="https://twitter.com/ClementAvenel"><img src="https://avatars.githubusercontent.com/u/29872940?v=4" width="100px;" alt="Clément Avenel"/><br /><sub><b>Clément Avenel</b></sub></a></td>
86
+ <td align="center" valign="top" width="25%"><a href="https://www.linkedin.com/in/pierre-fitoussi-267133135/"><img src="https://avatars.githubusercontent.com/u/79254731?v=4" width="100px;" alt="Pierre Fitoussi"/><br /><sub><b>Pierre Fitoussi</b></sub></a></td>
87
+ <td align="center" valign="top" width="25%"><a href="https://twitter.com/masterpoo_dev"><img src="https://avatars.githubusercontent.com/u/92919588?v=4" width="100px;" alt="Théo Dupuis"/><br /><sub><b>Théo Dupuis</b></sub></a></td>
88
+ </tr>
89
+ </table>
90
+
91
+
92
+ ## License
93
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ APP_RAKEFILE = File.expand_path('spec/dummy/Rakefile', __dir__)
6
+ load 'rails/tasks/engine.rake'
7
+
8
+ load 'rails/tasks/statistics.rake'
9
+
10
+ require 'bundler/gem_tasks'
@@ -0,0 +1 @@
1
+ /*! tailwindcss v3.3.3 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:Inter var,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:1rem}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:#0000}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{background-color:currentColor;border-color:#0000}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{background-color:currentColor;border-color:#0000}[type=file]{background:unset;border-color:inherit;border-radius:0;border-width:0;font-size:unset;line-height:inherit;padding:0}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{clip:rect(0,0,0,0);border-width:0;height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;white-space:nowrap;width:1px}.fixed{position:fixed}.relative{position:relative}.bottom-12{bottom:3rem}.right-12{right:3rem}.z-20{z-index:20}.-mx-1{margin-left:-.25rem;margin-right:-.25rem}.-mx-4{margin-left:-1rem;margin-right:-1rem}.-my-2{margin-bottom:-.5rem;margin-top:-.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.my-2{margin-bottom:.5rem;margin-top:.5rem}.my-4{margin-bottom:1rem;margin-top:1rem}.my-6{margin-bottom:1.5rem;margin-top:1.5rem}.my-8{margin-bottom:2rem;margin-top:2rem}.mb-12{margin-bottom:3rem}.mb-8{margin-bottom:2rem}.mr-1{margin-right:.25rem}.mr-4{margin-right:1rem}.mr-8{margin-right:2rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-3{-moz-column-gap:.75rem;column-gap:.75rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-r{border-right-width:1px}.border-t-4{border-top-width:4px}.border-solid{border-style:solid}.border-blue-600{--tw-border-opacity:1;border-color:rgb(37 99 235/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(219 234 254/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(239 246 255/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-2\.5{padding-bottom:.625rem;padding-top:.625rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-3\.5{padding-bottom:.875rem;padding-top:.875rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-8{padding-bottom:2rem;padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.align-middle{vertical-align:middle}.text-5xl{font-size:3rem;line-height:1}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-light{font-weight:300}.font-medium{font-weight:500}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.text-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-600{--tw-text-opacity:1;color:rgb(22 163 74/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.placeholder-gray-400\/70::-moz-placeholder{color:#9ca3afb3}.placeholder-gray-400\/70::placeholder{color:#9ca3afb3}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}h1{font-size:2.25rem;font-weight:700;line-height:2.5rem}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-blue-400:focus{--tw-border-opacity:1;border-color:rgb(96 165 250/var(--tw-border-opacity))}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(147 197 253/var(--tw-ring-opacity))}.focus\:ring-blue-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(96 165 250/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity))}.focus\:ring-opacity-40:focus{--tw-ring-opacity:0.4}.focus\:ring-offset-2:focus{--tw-ring-offset-width:2px}:is([dir=rtl] .rtl\:-scale-x-100){--tw-scale-x:-1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}:is([dir=rtl] .rtl\:border-l){border-left-width:1px}:is([dir=rtl] .rtl\:border-r-0){border-right-width:0}:is([dir=rtl] .rtl\:text-right){text-align:right}@media (min-width:640px){.sm\:-mx-6{margin-left:-1.5rem;margin-right:-1.5rem}.sm\:mt-0{margin-top:0}.sm\:flex{display:flex}.sm\:w-auto{width:auto}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}}@media (min-width:768px){.md\:rounded-lg{border-radius:.5rem}.md\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media (min-width:1024px){.lg\:-mx-8{margin-left:-2rem;margin-right:-2rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
@@ -0,0 +1 @@
1
+ //= link_tree ../builds/ .css
@@ -0,0 +1,17 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /*
6
+
7
+ @layer components {
8
+ .btn-primary {
9
+ @apply py-2 px-4 bg-blue-200;
10
+ }
11
+ }
12
+
13
+ */
14
+
15
+ h1 {
16
+ @apply text-4xl font-bold;
17
+ }
@@ -0,0 +1,26 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_self
15
+ */
16
+
17
+ @keyframes fade-out { 0% {opacity: 1;} 90% {opacity: 1;} 100% {opacity: 0;} }
18
+
19
+ .fade-out {
20
+ animation-name: fade-out;
21
+ }
22
+
23
+ .animated {
24
+ animation-duration: 5s;
25
+ animation-fill-mode: both;
26
+ }
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ require 'octokit'
5
+ require 'httparty'
6
+ class ApplicationController < ActionController::Base
7
+ add_flash_types :alert, :info, :error, :warning, :success
8
+ before_action :authenticate!, skip: :authenticate!
9
+ rescue_from ::Octokit::Unauthorized, with: :clear_session
10
+
11
+ CLIENT_ID = Rails.application.credentials.github.client_id
12
+ private_constant :CLIENT_ID
13
+
14
+ private
15
+
16
+ def clear_session
17
+ session.clear
18
+ redirect_to(dashboard_index_path)
19
+ end
20
+
21
+ def set_client
22
+ @client = GithubApiService.new(access_token:)
23
+ end
24
+
25
+ def access_token
26
+ session[:access_token]
27
+ end
28
+
29
+ def authenticate!
30
+ return if access_token
31
+
32
+ redirect_to("https://github.com/login/oauth/authorize?scope=repo,user&client_id=#{CLIENT_ID}", allow_other_host: true)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ class BranchesController < ApplicationController
5
+ before_action :set_client, only: %i[index show]
6
+
7
+ def index
8
+ redirect_to(branch_path(@client.default_branch))
9
+ end
10
+
11
+ def select
12
+ redirect_to(branch_path(params[:branch_id]))
13
+ end
14
+
15
+ def show
16
+ @branches = @client.public_branches
17
+ @branch_name = params[:id]
18
+ locales_yml = @client.pull(branch: @branch_name).values.map { YAML.load(_1) }
19
+ @code_value_by_locales = locales_yml.to_h { [_1.keys[0], LocaleHelper.traverse(_1)] }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ class DashboardController < ApplicationController
5
+ before_action :set_client, only: [:index]
6
+
7
+ def index
8
+ @locales_count = LocaleHelper.locales_count(@client)
9
+ translation_branches = @client.branches.filter { |branch| branch.ends_with?('__translations') }
10
+ @branches_count = @client.branches.count - translation_branches.count
11
+ @commits_count = translation_branches.sum do |branch|
12
+ @client.client.commits_since(@client.repository_fullname,
13
+ 1.month.ago.strftime('%Y-%m-%d'),
14
+ sha_or_branch: branch
15
+ ).count
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ require 'json'
5
+ require 'cgi'
6
+ class LocalesController < ApplicationController
7
+ before_action :set_client, only: %i[show update]
8
+
9
+ CLIENT_ID = Rails.application.credentials.github.client_id
10
+ CLIENT_SECRET = Rails.application.credentials.github.client_secret
11
+
12
+ private_constant :CLIENT_ID
13
+ private_constant :CLIENT_SECRET
14
+
15
+ def show
16
+ @locale = params[:locale]
17
+ @branch_name = params[:branch_id]
18
+ @source, @target = LocaleHelper.all_keys_for_locales(@client, [I18n.default_locale.to_s, @locale])
19
+ @translations = @target.zip(@source)
20
+ end
21
+
22
+ def update
23
+ @branch_name = params[:branch_id]
24
+ translation_keys = params[:val].permit!.to_h.compact_blank
25
+ yml = LocaleHelper.keys2yml(translation_keys)
26
+ yml.each { |path, file| @client.push(path, file, branch: @branch_name) }
27
+ @client.pull_request(@branch_name)
28
+ flash[:success] = t('.success')
29
+
30
+ redirect_to(branch_path(@branch_name))
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ class SessionController < ApplicationController
5
+ skip_before_action :authenticate!, only: %i[login logout]
6
+
7
+ CLIENT_ID = Rails.application.credentials.github.client_id
8
+ CLIENT_SECRET = Rails.application.credentials.github.client_secret
9
+
10
+ private_constant :CLIENT_ID
11
+ private_constant :CLIENT_SECRET
12
+
13
+ def login
14
+ code = params['code']
15
+ response = ::HTTParty.post('https://github.com/login/oauth/access_token',
16
+ body: {
17
+ client_id: CLIENT_ID,
18
+ client_secret: CLIENT_SECRET,
19
+ code:
20
+ }
21
+ )
22
+ parsed = CGI.parse(response)
23
+ session[:access_token] = parsed['access_token'].first
24
+ user = GithubApiService.new(access_token:).user
25
+ session[:user] = user.to_h.slice(:avatar_url, :id, :login)
26
+ redirect_to(root_path)
27
+ end
28
+
29
+ def logout
30
+ session.clear
31
+ redirect_to('https://github.com/logout')
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ module ApplicationHelper
5
+ def country_flag(locale)
6
+ locale.upcase.codepoints.map { |c| c + (0x1F1E6 - 65) }.pack('U*')
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ module DashboardHelper
5
+ end
6
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ module LocaleHelper
5
+ def self.keys2yml(translation_keys)
6
+ files = translation_keys.group_by { |key, _| key.split('$').first }.transform_values(&:to_h)
7
+ files.transform_values! { |translations| translations.transform_keys { |key| key.split('$').last } }
8
+ files.transform_values! do |file|
9
+ file.each_with_object({}) do |(key, value), hash|
10
+ hash.deep_merge!(key.split('.').reverse.reduce(value) { |a, n| { n => a } })
11
+ end.to_yaml
12
+ end
13
+ end
14
+
15
+ def self.locales_count(github_service)
16
+ github_service.locale_files_path.uniq { |x| x.scan(/\w.yml/) }.size
17
+ end
18
+
19
+ def self.all_keys(github_service, branch: 'translations')
20
+ locales_yml = github_service.pull(branch:).transform_values { |file| YAML.load(file) }
21
+ locales_list = locales_yml.values.map(&:keys).flatten.uniq
22
+ locales_yml.flat_map do |path, file|
23
+ path = path.gsub(/\b(#{locales_list.join('|')})\b/, '%<locale>s')
24
+ hash2keys(file.values.first).map { |key| "#{path}$%<locale>s.#{key}" }
25
+ end.uniq
26
+ end
27
+
28
+ def self.all_keys_for_locales(github_service, locales, branch: 'translations')
29
+ locales_yml = github_service.pull(branch:).transform_values { |file| YAML.load(file) }
30
+ locales_list = locales_yml.values.map(&:keys).flatten.uniq
31
+ locales_yml.transform_values! { |hash| [hash.keys.first, traverse(hash.values.first).to_h] }
32
+ generic_keys = locales_yml.flat_map do |path, file|
33
+ path = path.gsub(/\b(#{locales_list.join('|')})\b/, '%<locale>s')
34
+ _, hash = file
35
+ hash.map { |key, _hash| "#{path}$%<locale>s.#{key}" }
36
+ end.uniq
37
+ translations = locales_yml.flat_map do |path, (locale, hash)|
38
+ hash.map do |key, value|
39
+ ["#{path}$#{locale}.#{key}", value]
40
+ end
41
+ end.to_h
42
+
43
+ locales.map do |locale|
44
+ generic_keys.to_h do |key|
45
+ locale_key = format(key, locale:)
46
+ [locale_key, translations[locale_key]]
47
+ end
48
+ end
49
+ end
50
+
51
+ def self.missing_keys(locale, github_service, branch:)
52
+ generic_keys = all_keys(github_service, branch:)
53
+ locale_yml = pull_one_locale(locale, github_service, branch:)
54
+ locale_keys = locale_yml.flat_map do |path, file|
55
+ hash2keys(file).map { |key| "#{path}$#{key}" }
56
+ end.uniq
57
+ generic_keys.map { |key| format(key, locale:) } - locale_keys
58
+ end
59
+
60
+ def self.pull_one_locale(locale, github_service, branch: 'translations')
61
+ locale_files_path = github_service.locale_files_path(branch:).filter { |path| path.ends_with?("#{locale}.yml") }
62
+ github_service.pull(locale_files_path, branch:).transform_values { |file| YAML.load(file) }
63
+ end
64
+
65
+ def self.traverse(hash, parent_key = nil)
66
+ path = []
67
+ hash.each do |key, value|
68
+ current_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
69
+ if value.is_a?(Hash)
70
+ path += traverse(value, current_key)
71
+ else
72
+ path << [current_key, value]
73
+ end
74
+ end
75
+ path
76
+ end
77
+
78
+ def self.hash2keys(hash, parent_key = nil)
79
+ keys = []
80
+ hash.each do |key, value|
81
+ current_key = parent_key ? "#{parent_key}.#{key}" : key.to_s
82
+ if value.is_a?(Hash)
83
+ keys += hash2keys(value, current_key)
84
+ else
85
+ keys << current_key
86
+ end
87
+ end
88
+ keys
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ class ApplicationJob < ActiveJob::Base
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ class ApplicationMailer < ActionMailer::Base
5
+ default from: 'from@example.com'
6
+ layout 'mailer'
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LocaleNinja
4
+ class GithubApiService
5
+ REPOSITORY_FULLNAME = Rails.application.credentials.github.repository_name
6
+ private_constant :REPOSITORY_FULLNAME
7
+
8
+ def initialize(access_token:)
9
+ @client = ::Octokit::Client.new(access_token:)
10
+ end
11
+
12
+ attr_reader :client
13
+
14
+ def user
15
+ @client.user
16
+ end
17
+
18
+ def translation_branch(branch)
19
+ branch.ends_with?('__translations') ? branch : "#{branch}__translations"
20
+ end
21
+
22
+ def locale_files_path(dir = 'config/locales', branch: 'translations')
23
+ branch = translation_branch(branch) if branch?(translation_branch(branch))
24
+ @client.contents(repository_fullname, path: dir, ref: "heads/#{branch}").map do |file|
25
+ if file.type == 'dir'
26
+ locale_files_path(file.path, branch:)
27
+ else
28
+ file.path
29
+ end
30
+ end.flatten
31
+ end
32
+
33
+ def pull(files = nil, branch: 'translations')
34
+ files ||= locale_files_path(branch:)
35
+ branch = translation_branch(branch) if branch?(translation_branch(branch))
36
+
37
+ files.index_with { |path| Base64.decode64(@client.contents(repository_fullname, path:, ref: "heads/#{branch}").content) }
38
+ end
39
+
40
+ def create_translation_branch(branch)
41
+ return branch if branch.ends_with?('__translations')
42
+
43
+ translation_branch = "#{branch}__translations"
44
+ return translation_branch if branch?(translation_branch)
45
+
46
+ create_branch(branch, translation_branch)
47
+ translation_branch
48
+ end
49
+
50
+ def push(file_path, content, branch: 'translations')
51
+ branch = create_translation_branch(branch)
52
+
53
+ begin
54
+ sha = @client.content(repository_fullname, path: file_path, ref: "heads/#{branch}")[:sha]
55
+ rescue ::Octokit::NotFound
56
+ create_file(file_path, content, branch:)
57
+ return
58
+ end
59
+ @client.update_contents(repository_fullname, file_path, "translations #{DateTime.current}", sha, content, branch:)
60
+ end
61
+
62
+ def create_file(file_path, content, branch: 'translations')
63
+ @client.create_contents(repository_fullname, file_path, "translations #{DateTime.current}", content, branch:)
64
+ end
65
+
66
+ def create_branch(parent_branch, child_branch)
67
+ sha = @client.ref(repository_fullname, "heads/#{parent_branch}").dig(:object, :sha)
68
+ @client.create_ref(repository_fullname, "heads/#{child_branch}", sha)
69
+ @branches << child_branch
70
+ end
71
+
72
+ def branch?(branch_name)
73
+ branches.include?(branch_name)
74
+ end
75
+
76
+ def repository_fullname
77
+ @repository_fullname ||= REPOSITORY_FULLNAME
78
+ end
79
+
80
+ def branches
81
+ @branches ||= @client.branches(repository_fullname).map(&:name)
82
+ end
83
+
84
+ def default_branch
85
+ %w[main master].find { |branch_name| branches.include?(branch_name) } || branches.first
86
+ end
87
+
88
+ def public_branches
89
+ branches.reject { |branch| branch.ends_with?('__translations') }
90
+ end
91
+
92
+ def pull_request(branch_name)
93
+ @client.create_pull_request(repository_fullname, branch_name, "#{branch_name}__translations", "translations #{Time.current}")
94
+ rescue ::Octokit::UnprocessableEntity
95
+ # If pull request already exists, do nothing
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,10 @@
1
+ <div class="mb-12 py-4 px-6 rounded-md shadow-md border-solid ">
2
+ <!-- <%= render partial: "/components/table" %> -->
3
+ <%= form_with url: branch_select_path, class: "w-full flex justify-between" do |f| %>
4
+ <div class="flex items-center">
5
+ <label class="branch-selection text-lg mr-4">Choose a branch:</label>
6
+ <%= f.select :branch_id, options_for_select(@branches, @branch_name || 'main'), {}, onchange: 'selectionChange(this)', class: "w-80 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5" %>
7
+ </div>
8
+ <%= f.submit "Fetch this branch", class: "text-white bg-blue-600 px-4 py-3 font-bold rounded cursor-pointer" %>
9
+ <% end %>
10
+ </div>
@@ -0,0 +1,33 @@
1
+ <div class="fixed bottom-12 right-12 z-20">
2
+ <% flash.each do |type, message| %>
3
+ <div class="flex w-full max-w-sm bg-white rounded-md shadow-card my-2 fade-out animated">
4
+ <div role="alert" class="rounded-xl border border-gray-100 p-4 shadow-xl">
5
+ <div class="flex items-start gap-4">
6
+ <% case type %>
7
+ <% when "notice", "success" %>
8
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-green-600">
9
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
10
+ </svg>
11
+ <% when "info" %>
12
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-primary-blue">
13
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
14
+ </svg>
15
+ <% when "warning" %>
16
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-dark-yellow">
17
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
18
+ </svg>
19
+ <% when "alert", "error" %>
20
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-red-500">
21
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
22
+ </svg>
23
+ <% else %>
24
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-gray-700">
25
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
26
+ </svg>
27
+ <% end %>
28
+ <p class="text-sm text-gray-700"><%= message %></p>
29
+ </div>
30
+ </div>
31
+ </div>
32
+ <% end %>
33
+ </div>